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/.gitignore b/.gitignore
new file mode 100644
index 000000000..d40623ab1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+**/bower_components
+/build
+/node_modules/
+/src/metrics_server/node_modules/
+/src/server_manager/node_modules/
+/src/server_manager/install_scripts/do_install_script.ts
+yarn-error.log
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..3a1fd607b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,135 @@
+language: node_js
+
+node_js:
+ - "8"
+
+cache:
+ yarn: true
+ directories:
+ - $HOME/.cache/bower
+ - $HOME/.cache/electron
+ - $HOME/.cache/electron-builder
+
+before_install:
+ # https://docs.travis-ci.com/user/languages/javascript-with-nodejs#Travis-CI-supports-yarn
+ - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.3.2
+ - export PATH="$HOME/.yarn/bin:$PATH"
+
+stages:
+ - build and unit test
+ - integration test
+ - name: tag
+ if: type = cron
+ - name: deploy
+ if: tag =~ ^daily
+ - name: release
+ if: tag =~ ^v[0-9]
+
+# Stages with the same name define multiple jobs which run in parallel.
+# To make it more apparent in the Travis UI exactly what each job is
+# doing, we add a descriptive environment variable.
+jobs:
+ include:
+ # Ideally, we would split this stage in some way, e.g. by component or by
+ # build/test commands, to make it clearer in the Travis UI exactly which
+ # command failed. However, since each stage incurs a significantly start-up
+ # cost, we combine test and build commands for all components into one fast
+ # stage.
+ - stage: build and unit test
+ script:
+ - yarn shadowbox_install
+ - yarn do shadowbox/server/build
+ - yarn do shadowbox/test
+ - yarn do server_manager/electron_app/build
+ - yarn do server_manager/web_app/build
+ - yarn do server_manager/web_app/test
+
+ - stage: integration test
+ sudo: required
+ services: docker
+ script:
+ # https://docs.travis-ci.com/user/docker/
+ - |
+ sudo rm -f /usr/local/bin/docker-compose
+ curl -L https://github.com/docker/compose/releases/download/1.17.1/docker-compose-$(uname -s)-$(uname -m) > docker-compose
+ chmod +x docker-compose
+ sudo mv docker-compose /usr/local/bin
+ - yarn shadowbox_install
+ - yarn do shadowbox/integration_test/run
+
+ - stage: tag
+ script:
+ - RELEASE_NAME=daily-$(date -I)
+ - curl --data '{"tag_name":"'$RELEASE_NAME'","name":"'$RELEASE_NAME'","prerelease":true}' https://api.github.com/repos/Jigsaw-Code/outline-server/releases?access_token=$CI_USER_TOKEN
+
+ - stage: deploy
+ env:
+ - DESC=shadowbox docker image
+ sudo: required
+ services: docker
+ script:
+ - yarn shadowbox_install
+ - yarn do shadowbox/docker/build
+ - docker login quay.io -u="$QUAY_IO_USERNAME" -p="$QUAY_IO_PASSWORD"
+ - docker tag outline/shadowbox quay.io/outline/shadowbox:$TRAVIS_TAG
+ - docker push quay.io/outline/shadowbox:$TRAVIS_TAG
+ - docker tag outline/shadowbox quay.io/outline/shadowbox:daily
+ - docker push quay.io/outline/shadowbox:daily
+
+ - stage: deploy
+ env:
+ - DESC=linux manager
+ addons:
+ apt:
+ packages:
+ - rpm
+ script: yarn do server_manager/electron_app/package_linux
+
+ # https://www.electron.build/multi-platform-build
+ - stage: deploy
+ env:
+ - DESC=windows manager
+ sudo: required
+ services: docker
+ script:
+ - yarn do server_manager/electron_app/build
+ - docker pull electronuserland/builder:wine
+ - docker run --rm
+ -v ${PWD}:/project
+ -v ~/.cache/electron:/root/.cache/electron
+ -v ~/.cache/electron-builder:/root/.cache/electron-builder
+ electronuserland/builder:wine
+ /bin/bash -c "yarn do server_manager/electron_app/package_only_windows" || travis_terminate $?
+
+ - stage: deploy
+ env:
+ - DESC=macos manager
+ os: osx
+ script: yarn do server_manager/electron_app/package_macos
+
+ # Note that because we cannot currently build signed macOS or Windows
+ # binaries on Travis, those builds must be manually built and uploaded
+ # to the release page.
+ - stage: release
+ env:
+ - DESC=linux manager
+ addons:
+ apt:
+ packages:
+ - rpm
+ script: yarn do server_manager/electron_app/release_linux
+
+deploy:
+ provider: releases
+ api_key:
+ secure: "a7JJwbwgWQpXAaGCKbMf/HySyhiBOPeyjZZkSeZBECpqS671j6rbZ2MHvXp7QfU3LZ+Z7MYwkl/DTgbdOZ8ndbwWMn5yJjeIBnreQqZlbyR5jh1G66vu+r55aBxd6+svGp2VynGlWLI+1+4L6U33VHNXnkH6D7fwSKze1glu1XqeUzUkNEPRkWCg/Y6/WGMh19yOgoxulN3mTL65s5FzFpXKdDT7F8J6BPzoz5cWXTMiZM+fs0BjTIfNNabkIWdRvVFJ5s2Cx3EJM0BU1NDRVxGeYJsvli/gkYW82ZTeQdXfn9KQxAK1n6lQsUQJUnErH6jSfrv6QJkSnnKjVogXcP/SSj0p73UAcZuUZ7hRW/TX0HAtgCxnY7dkMaxyBHiNwxprSm4+83VRHIALzUqmcJ28b6VvXo1znD3r9frDsY5PuNAmew3VbpQyxit515tZRpiRXzzSnrFqAovWl6wY0UIkQEFLpi18PGOhUFcewKBpN4W4PszGvWkPgdPNBq5nizUtaHX62lPFoPjGkhcunD4Tn9RIrUcbRTfWVokI5qT8oLZM3mgBS8H85N80ngq1tQBUCJ1Xfd3vqJ+xE0x6boIzYHs4cz0Qoao1vmo1wsycRhr1PAqvtBztAkq+pwOwd9qI9VL8QCJErHBd56I3jkPTsPgMx8K3M8N4/EtOsA0="
+ file_glob: true
+ file: "build/server_manager/electron_app/static/dist/*.*"
+ skip_cleanup: true
+ on:
+ tags: true
+
+env:
+ global:
+ - ELECTRON_CACHE=$HOME/.cache/electron
+ - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..6d364e1dd
--- /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.
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 261eeb9e9..8dada3eda 100644
--- a/LICENSE
+++ b/LICENSE
@@ -178,7 +178,7 @@
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 "[]"
+ 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
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright [yyyy] [name of copyright owner]
+ 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.
diff --git a/README.md b/README.md
index cdd32fcc7..d45427f8b 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,115 @@
-# outline-server
-Create and manage access to Outline servers
+# Outline Server Creator
+
+[![Build Status](https://travis-ci.com/Jigsaw-Code/outline-server.svg?token=HiP4RTme8LSvyrP9kNJq&branch=master)](https://travis-ci.com/Jigsaw-Code/outline-server)
+
+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.
+
+
+## Components
+
+The system comprises the following components:
+
+- **Server Manager Electron App:** an Election application that wraps the Server Manager web application and runs
+ natively on Desktop. It adds some extra functionality, like validation of the server self-signed certificate and interception of the DigitalOcean registration flow.
+
+ Code: `src/server_manager/electron_app`
+- **Proxy Server**: a server that runs the Shadowsocks instances and a REST API to manage its users. Used as backend by the
+ Server Manager app.
+
+ Code: `src/shadowbox`
+
+## Server Manager
+
+### Setup
+
+Ensure you have the following installed:
+ - [Node](https://nodejs.org/)
+ - [Yarn](https://yarnpkg.com/en/docs/install)
+ - [Wine](https://www.winehq.org/download), if you would like to generate binaries for Windows.
+
+Install dependencies:
+```
+yarn
+```
+
+### Electron App
+
+To run the electron app:
+```
+yarn do server_manager/electron_app/run
+```
+
+To build the app for all platforms:
+```
+yarn do server_manager/electron_app/package
+```
+
+The per-platform standalone apps will be at `build/electron_app/bundled`.
+
+The per-platform standalone apps packaged for distribution will be at
+`build/electron_app/packaged` in the following formats:
+
+- 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.
+
+To perform a release, use
+```
+yarn do server_manager/electron_app/release
+```
+
+This will perform a clean and reinstall all dependencies to make sure the build is not tainted.
+
+
+## Proxy Server
+
+See [`src/shadowbox/README.md`](src/shadowbox/README.md).
+
+## Unit Tests
+
+To run all the tests, run `yarn test`
+
+
+## Build System
+
+We have a very simple build system based on package.json scripts that are called called using `yarn`
+and a thin wrapper for what we call build "actions".
+
+We've defined a `do` package.json script that takes an `action` parameter:
+```shell
+yarn do $ACTION
+```
+
+This command will define a `do_action()` function and call `${ACTION}_action.sh`, which must exist.
+The called action script can use `do_action` to call its dependencies. The $ACTION parameter is
+always resolved from the project root, regardless of the caller location.
+
+The idea of `do_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.
+
+### 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:
+```
+yarn run clean
+```
diff --git a/jasmine.json b/jasmine.json
new file mode 100644
index 000000000..f80f1ee8d
--- /dev/null
+++ b/jasmine.json
@@ -0,0 +1,9 @@
+{
+ "spec_dir": ".",
+ "spec_files": [
+ "build/**/*.spec.js"
+ ],
+ "helpers": ["src/base64mocks.js"],
+ "stopSpecOnExpectationFailure": false,
+ "random": false
+}
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..025a50793
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "outline-server",
+ "devDependencies": {
+ "@types/jasmine": "^2.5.53",
+ "clang-format": "^1.2.2",
+ "husky": "^0.14.3",
+ "jasmine": "^2.6.0",
+ "tslint": "^5.9.1",
+ "typescript": "^2.6.2"
+ },
+ "scripts": {
+ "postinstall": "yarn run server_manager_install",
+ "server_manager_install": "cd src/server_manager && yarn install --modules-folder ../../node_modules && yarn bower install",
+ "clean": "rm -rf src/server_manager/bower_components/ src/{metrics_server,server_manager}/node_modules/ build/ node_modules/ src/server_manager/install_scripts/do_install_script.ts",
+ "shadowbox_install": "cd src/shadowbox && yarn install --modules-folder ../../node_modules --no-bin-links",
+ "metrics_server_install": "cd src/metrics_server && yarn install --modules-folder ../../node_modules",
+ "do": "bash ./scripts/do_action.sh",
+ "precommit": "for i in . src/server_manager/electron_app src/metrics_server src/shadowbox; do tslint -p $i --fix; done; git-clang-format"
+ }
+}
diff --git a/scripts/do_action.sh b/scripts/do_action.sh
new file mode 100755
index 000000000..40604eb45
--- /dev/null
+++ b/scripts/do_action.sh
@@ -0,0 +1,37 @@
+#!/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 -eux
+
+# 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".
+
+export ROOT_DIR=${ROOT_DIR:-$(git rev-parse --show-toplevel)}
+export BUILD_DIR=${BUILD_DIR:-$ROOT_DIR/build}
+
+function do_action() {
+ readonly STYLE_BOLD_WHITE='\033[1;37m'
+ readonly STYLE_RESET='\033[0m'
+ local action=$1
+ echo -e "$STYLE_BOLD_WHITE[Running $action]$STYLE_RESET"
+ shift
+ $ROOT_DIR/src/${action}_action.sh "$@"
+ echo -e "$STYLE_BOLD_WHITE[Done $action]$STYLE_RESET"
+}
+export -f do_action
+
+do_action "$@"
diff --git a/src/metrics_server/README.md b/src/metrics_server/README.md
new file mode 100644
index 000000000..e873ee994
--- /dev/null
+++ b/src/metrics_server/README.md
@@ -0,0 +1,51 @@
+# Outline Metrics Server
+
+The Outline Metrics Server is built using [Google Cloud Functions](https://cloud.google.com/functions/), which lets us write a simple Node HTTP server. By deploying this server to the uproxysite Google project, we gain permission to write to uproxysite's BigQuery tables.
+
+## Requirements
+* Install `gcloud` from https://cloud.google.com/sdk/docs/
+* Node 6.11.1 or greater (for testing via Cloud Functions Emulator)
+* You must run `yarn metrics_server_install` explicitly to install metrics_server dependencies.
+
+## Building
+Run `yarn do metrics_server/build`
+
+## Deploying
+To deploy
+* Authenticate with gcloud: `gcloud auth login`
+* Select the gcloud project to uproxysite: `gcloud config set project uproxysite`
+* Run the deploy script:
+ * to deploy to test: `yarn do metrics_server/deploy_test`
+ * to deploy to prod: `yarn do metrics_server/deploy_prod`
+
+## Testing with the Cloud Functions Emulator
+You can test with the Google Cloud Functions Emulator by running `yarn do metrics_server/test `, e.g.:
+```
+yarn do metrics_server/test '{"serverId":"12345","startUtcMs":1502486354823,"endUtcMs":1502499314823,"userReports":[{"userId":"1","bytesTransferred":60,"countries":["US","NL"]},{"userId":"2","bytesTransferred":100,"countries":["UK"]}]}'
+```
+
+## Testing with Node
+You can test the metrics server code using Node:
+
+`cd build/metrics_server`
+
+run `node`, then you can test the post_server_report module as follows:
+```
+post_server_report = require('./post_server_report.js');
+
+serverReport = {
+ serverId: "123",
+ startUtcMs: 1502486354823,
+ endUtcMs: 1502499314823,
+ userReports: [
+ {userId: "1", bytesTransferred: 60, countries: ["US", "NL"]},
+ {userId: "2", bytesTransferred: 100, countries: ["CN"]}
+ ]
+}
+
+post_server_report.postServerReport('uproxy_metrics_test', 'connections_v1', serverReport)
+ .then(() => { console.log('success') })
+ .catch((e) => { console.error('failure: ' + e) })
+```
+
+You can then view this inserted data at https://bigquery.cloud.google.com/table/uproxysite:uproxy_metrics_test.connections_v1
diff --git a/src/metrics_server/build.sh b/src/metrics_server/build.sh
new file mode 100755
index 000000000..5e60ed90b
--- /dev/null
+++ b/src/metrics_server/build.sh
@@ -0,0 +1,33 @@
+#!/bin/bash -eux
+#
+# 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.
+
+if (( $# <= 1 )); then
+ echo "Invalid arguments, usage:"
+ echo "build.sh "
+ exit 1;
+fi
+
+readonly MODULE_DIR=$(dirname $0)
+readonly OUT_DIR=$1
+readonly CONFIG_FILE=$2
+
+# Compile the server.
+rm -rf $OUT_DIR
+tsc -p $MODULE_DIR/tsconfig.json --outDir $OUT_DIR
+cp -r $MODULE_DIR/package.json $OUT_DIR
+
+# Copy config file.
+cp -r $CONFIG_FILE $OUT_DIR/config.json
diff --git a/src/metrics_server/config_prod.json b/src/metrics_server/config_prod.json
new file mode 100644
index 000000000..7c7056efa
--- /dev/null
+++ b/src/metrics_server/config_prod.json
@@ -0,0 +1,4 @@
+{
+ "datasetName": "uproxy_metrics",
+ "tableName": "connections_v1"
+}
diff --git a/src/metrics_server/config_test.json b/src/metrics_server/config_test.json
new file mode 100644
index 000000000..3071fd06d
--- /dev/null
+++ b/src/metrics_server/config_test.json
@@ -0,0 +1,4 @@
+{
+ "datasetName": "uproxy_metrics_test",
+ "tableName": "connections_v1"
+}
diff --git a/src/metrics_server/deploy_prod_action.sh b/src/metrics_server/deploy_prod_action.sh
new file mode 100755
index 000000000..e35cec8a9
--- /dev/null
+++ b/src/metrics_server/deploy_prod_action.sh
@@ -0,0 +1,25 @@
+#!/bin/bash -eux
+#
+# 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 MODULE_DIR=$(dirname $0)
+readonly OUT_DIR=$BUILD_DIR/metrics_server/prod
+readonly CONFIG_FILE=$MODULE_DIR/config_prod.json
+
+# Build the server
+$MODULE_DIR/build.sh $OUT_DIR $CONFIG_FILE
+
+# Deploy as "reportHourlyConnectionMetrics"
+gcloud beta functions deploy reportHourlyConnectionMetrics --stage-bucket uproxy-cloud-functions --trigger-http --source=$OUT_DIR --entry-point=reportHourlyConnectionMetrics
diff --git a/src/metrics_server/deploy_test_action.sh b/src/metrics_server/deploy_test_action.sh
new file mode 100755
index 000000000..e3a09579b
--- /dev/null
+++ b/src/metrics_server/deploy_test_action.sh
@@ -0,0 +1,25 @@
+#!/bin/bash -eux
+#
+# 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 MODULE_DIR=$(dirname $0)
+readonly OUT_DIR=$BUILD_DIR/metrics_server/test
+readonly CONFIG_FILE=$MODULE_DIR/config_test.json
+
+# Build the server
+$MODULE_DIR/build.sh $OUT_DIR $CONFIG_FILE
+
+# Deploy as "reportHourlyConnectionMetricsTest"
+gcloud beta functions deploy reportHourlyConnectionMetricsTest --stage-bucket uproxy-cloud-functions --trigger-http --source=$OUT_DIR --entry-point=reportHourlyConnectionMetrics
diff --git a/src/metrics_server/index.ts b/src/metrics_server/index.ts
new file mode 100644
index 000000000..1a8beb1bd
--- /dev/null
+++ b/src/metrics_server/index.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 * as express from 'express';
+import * as fs from 'fs';
+import * as path from 'path';
+import {HourlyServerMetricsReport, isValidServerReport, postServerReport} from './post_server_report';
+
+// Accepts hourly connection metrics and inserts them into BigQuery.
+// Request body should contain an HourlyServerMetricsReport.
+exports.reportHourlyConnectionMetrics = (req: express.Request, res: express.Response) => {
+ if (req.method !== 'POST') {
+ res.status(405).send('Method not allowed');
+ return;
+ }
+ if (!isValidServerReport(req.body)) {
+ res.status(400).send('Invalid request');
+ return;
+ }
+
+ const serverReport: HourlyServerMetricsReport = {
+ serverId: req.body.serverId,
+ startUtcMs: req.body.startUtcMs,
+ endUtcMs: req.body.endUtcMs,
+ userReports: req.body.userReports
+ };
+ postServerReport(config.datasetName, config.tableName, serverReport).then(() => {
+ res.status(200).send('OK');
+ }).catch((err: Error) => {
+ res.status(500).send('Error: ' + err);
+ });
+};
+
+interface Config {
+ datasetName: string;
+ tableName: string;
+}
+
+function loadConfig(): Config {
+ const configText = fs.readFileSync(path.join(__dirname, 'config.json'), {encoding: 'utf8'});
+ return JSON.parse(configText);
+}
+
+const config = loadConfig();
diff --git a/src/metrics_server/package.json b/src/metrics_server/package.json
new file mode 100644
index 000000000..36ba3ee89
--- /dev/null
+++ b/src/metrics_server/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "outline-metrics-server",
+ "private": true,
+ "version": "0.1.0",
+ "description": "Outline metrics server",
+ "author": "Outline",
+ "license": "Apache",
+ "dependencies": {
+ "@google-cloud/bigquery": "^0.9.6"
+ },
+ "devDependencies": {
+ "@google-cloud/functions-emulator": "1.0.0-alpha.23",
+ "@types/express": "^4.0.36"
+ }
+}
diff --git a/src/metrics_server/post_server_report.ts b/src/metrics_server/post_server_report.ts
new file mode 100644
index 000000000..1a07b8645
--- /dev/null
+++ b/src/metrics_server/post_server_report.ts
@@ -0,0 +1,118 @@
+// 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 bigquery from '@google-cloud/bigquery';
+
+// TODO(dborkan): HourlyServerMetricsReport and HourlyUserMetricsReport are
+// copied from src/shadowbox/server/metrics.ts - find a way to share these
+// definitions between shadowbox and the metrics_server.
+export interface HourlyServerMetricsReport {
+ serverId: string;
+ startUtcMs: number;
+ endUtcMs: number;
+ userReports: HourlyUserMetricsReport[];
+}
+interface HourlyUserMetricsReport {
+ userId: string;
+ countries: string[];
+ bytesTransferred: number;
+}
+
+interface ConnectionRow {
+ serverId: string;
+ startTimestamp: string; // ISO formatted string.
+ endTimestamp: string; // ISO formatted string.
+ userId: string;
+ bytesTransferred: number;
+ countries: string[];
+}
+
+// Instantiates a client
+const bigqueryProject = bigquery({
+ projectId: 'uproxysite'
+});
+
+export function postServerReport(datasetName: string, tableName: string, serverReport: HourlyServerMetricsReport) {
+ const dataset = bigqueryProject.dataset(datasetName);
+ const table = dataset.table(tableName);
+ const rows = getConnectionRowsFromServerReport(serverReport);
+ return new Promise((fulfill, reject) => {
+ table.insert(rows, (err: Error) => {
+ if (err) {
+ reject(err);
+ } else {
+ fulfill();
+ }
+ });
+ });
+}
+
+function getConnectionRowsFromServerReport(serverReport: HourlyServerMetricsReport): ConnectionRow[] {
+ const startTimestampStr = new Date(serverReport.startUtcMs).toISOString();
+ const endTimestampStr = new Date(serverReport.endUtcMs).toISOString();
+ const rows = [];
+ for (const userReport of serverReport.userReports) {
+ rows.push({
+ serverId: serverReport.serverId,
+ startTimestamp: startTimestampStr,
+ endTimestamp: endTimestampStr,
+ userId: userReport.userId,
+ bytesTransferred: userReport.bytesTransferred,
+ countries: userReport.countries
+ });
+ }
+ return rows;
+}
+
+// Returns true iff testObject contains a valid HourlyServerMetricsReport.
+// tslint:disable-next-line:no-any
+export function isValidServerReport(testObject: any): boolean {
+ // Check that all required fields are present.
+ const requiredServerReportFields = ['serverId', 'startUtcMs', 'endUtcMs', 'userReports'];
+ for (const fieldName of requiredServerReportFields) {
+ if (!testObject[fieldName]) {
+ return false;
+ }
+ }
+
+ // Check that startUtcMs is not after endUtcMs.
+ if (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 requiredUserReportFields = ['userId', 'countries', 'bytesTransferred'];
+ const MIN_BYTES_TRANSFERRED = 0;
+ const MAX_BYTES_TRANSFERRED = 500 * Math.pow(2, 30); // 500 GB.
+ for (const userReport of testObject.userReports) {
+ // Test that each userReport contains valid fields.
+ for (const fieldName of requiredUserReportFields) {
+ if (!userReport[fieldName]) {
+ return false;
+ }
+ }
+ // Check that bytesTransferred is between min and max transfer limits
+ if (userReport.bytesTransferred < MIN_BYTES_TRANSFERRED ||
+ userReport.bytesTransferred > MAX_BYTES_TRANSFERRED) {
+ return false;
+ }
+ }
+
+ // Request is a valid HourlyServerMetricsReport.
+ return true;
+}
diff --git a/src/metrics_server/test_action.sh b/src/metrics_server/test_action.sh
new file mode 100755
index 000000000..888b8b2cd
--- /dev/null
+++ b/src/metrics_server/test_action.sh
@@ -0,0 +1,34 @@
+#!/bin/bash -eux
+#
+# 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.
+
+if (( $# <= 0 )); then
+ echo "No test data specified"
+ exit 1;
+fi
+
+readonly MODULE_DIR=$(dirname $0)
+readonly OUT_DIR=$BUILD_DIR/metrics_server/test
+readonly CONFIG_FILE=$MODULE_DIR/config_test.json
+
+# Build the server
+$MODULE_DIR/build.sh $OUT_DIR $CONFIG_FILE
+
+# TODO(dborkan): figure out why the functions binary isn't installed at $ROOT_DIR/node_modules/.bin/
+readonly FUNCTIONS_EMULATOR=$ROOT_DIR/node_modules/@google-cloud/functions-emulator/bin/functions
+
+$FUNCTIONS_EMULATOR start
+$FUNCTIONS_EMULATOR deploy reportHourlyConnectionMetrics --trigger-http --local-path=$OUT_DIR --entry-point=reportHourlyConnectionMetrics
+$FUNCTIONS_EMULATOR call reportHourlyConnectionMetrics --data=$1
diff --git a/src/metrics_server/tsconfig.json b/src/metrics_server/tsconfig.json
new file mode 100644
index 000000000..41064ab2a
--- /dev/null
+++ b/src/metrics_server/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "es2016",
+ "removeComments": false,
+ "noImplicitAny": true,
+ "module": "commonjs",
+ "rootDir": ".",
+ "lib": [
+ "dom",
+ "es2016"
+ ]
+ },
+ "include": [
+ "index.ts",
+ "post_server_report.ts",
+ "types/**/*.d.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ],
+ "compileOnSave": true
+}
diff --git a/src/metrics_server/types/@google-cloud.d.ts b/src/metrics_server/types/@google-cloud.d.ts
new file mode 100644
index 000000000..0e080c939
--- /dev/null
+++ b/src/metrics_server/types/@google-cloud.d.ts
@@ -0,0 +1,19 @@
+// 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.
+
+// TODO(dborkan): Find a *.d.ts file with this definition. The API is defined at
+// https://googlecloudplatform.github.io/google-cloud-node/#/docs/bigquery/0.9.6/bigquery
+declare module '@google-cloud/bigquery';
diff --git a/src/metrics_server/yarn.lock b/src/metrics_server/yarn.lock
new file mode 100644
index 000000000..40f38a1f9
--- /dev/null
+++ b/src/metrics_server/yarn.lock
@@ -0,0 +1,1981 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@google-cloud/bigquery@^0.9.6":
+ version "0.9.6"
+ resolved "https://registry.yarnpkg.com/@google-cloud/bigquery/-/bigquery-0.9.6.tgz#beac6d486c45f8010a480438de4945252fc4ceae"
+ dependencies:
+ "@google-cloud/common" "^0.13.0"
+ arrify "^1.0.0"
+ duplexify "^3.5.0"
+ extend "^3.0.0"
+ is "^3.0.1"
+ stream-events "^1.0.1"
+ string-format-obj "^1.0.0"
+
+"@google-cloud/common@^0.13.0":
+ version "0.13.4"
+ resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-0.13.4.tgz#75bb7f60931cfc9d94da0b5d408950d0bbf0e979"
+ dependencies:
+ array-uniq "^1.0.3"
+ arrify "^1.0.1"
+ concat-stream "^1.6.0"
+ create-error-class "^3.0.2"
+ duplexify "^3.5.0"
+ ent "^2.2.0"
+ extend "^3.0.0"
+ google-auto-auth "^0.7.1"
+ is "^3.2.0"
+ log-driver "^1.2.5"
+ methmeth "^1.1.0"
+ modelo "^4.2.0"
+ request "^2.79.0"
+ retry-request "^2.0.0"
+ split-array-stream "^1.0.0"
+ stream-events "^1.0.1"
+ string-format-obj "^1.1.0"
+ through2 "^2.0.3"
+
+"@google-cloud/functions-emulator@1.0.0-alpha.23":
+ version "1.0.0-alpha.23"
+ resolved "https://registry.yarnpkg.com/@google-cloud/functions-emulator/-/functions-emulator-1.0.0-alpha.23.tgz#5f2916934a31369318dc81c23eb917a0400f6ead"
+ dependencies:
+ "@google-cloud/storage" "1.2.1"
+ adm-zip "0.4.7"
+ ajv "5.2.2"
+ body-parser "1.17.2"
+ cli-table2 "0.2.0"
+ colors "1.1.2"
+ configstore "3.1.1"
+ express "4.15.4"
+ google-proto-files "0.12.1"
+ googleapis "20.1.0"
+ got "7.1.0"
+ grpc "1.4.1"
+ http-proxy "1.16.2"
+ lodash "4.17.4"
+ prompt "1.0.0"
+ rimraf "2.6.1"
+ semver "5.4.1"
+ serializerr "1.0.3"
+ tmp "0.0.31"
+ uuid "3.1.0"
+ winston "2.3.1"
+ yargs "8.0.2"
+
+"@google-cloud/storage@1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-1.2.1.tgz#a0f2e20871b862f0ea64a90ac48fc08845cf9505"
+ dependencies:
+ "@google-cloud/common" "^0.13.0"
+ arrify "^1.0.0"
+ async "^2.0.1"
+ concat-stream "^1.5.0"
+ create-error-class "^3.0.2"
+ duplexify "^3.5.0"
+ extend "^3.0.0"
+ gcs-resumable-upload "^0.8.0"
+ hash-stream-validation "^0.2.1"
+ is "^3.0.1"
+ mime-types "^2.0.8"
+ once "^1.3.1"
+ pumpify "^1.3.3"
+ stream-events "^1.0.1"
+ string-format-obj "^1.0.0"
+ through2 "^2.0.0"
+
+"@types/express-serve-static-core@*":
+ version "4.0.49"
+ resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.0.49.tgz#3438d68d26e39db934ba941f18e3862a1beeb722"
+ dependencies:
+ "@types/node" "*"
+
+"@types/express@^4.0.36":
+ version "4.0.36"
+ resolved "https://registry.yarnpkg.com/@types/express/-/express-4.0.36.tgz#14eb47de7ecb10319f0a2fb1cf971aa8680758c2"
+ dependencies:
+ "@types/express-serve-static-core" "*"
+ "@types/serve-static" "*"
+
+"@types/mime@*":
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.1.tgz#2cf42972d0931c1060c7d5fa6627fce6bd876f2f"
+
+"@types/node@*":
+ version "8.0.22"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.22.tgz#9c6bfee1f45f5e9952ff6b487e657ecca48c7777"
+
+"@types/serve-static@*":
+ version "1.7.31"
+ resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.7.31.tgz#15456de8d98d6b4cff31be6c6af7492ae63f521a"
+ dependencies:
+ "@types/express-serve-static-core" "*"
+ "@types/mime" "*"
+
+abbrev@1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"
+
+accepts@~1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca"
+ dependencies:
+ mime-types "~2.1.11"
+ negotiator "0.6.1"
+
+adm-zip@0.4.7:
+ version "0.4.7"
+ resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.7.tgz#8606c2cbf1c426ce8c8ec00174447fd49b6eafc1"
+
+ajv@5.2.2:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^1.0.0"
+ json-schema-traverse "^0.3.0"
+ json-stable-stringify "^1.0.1"
+
+ajv@^4.9.1:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
+ dependencies:
+ co "^4.6.0"
+ json-stable-stringify "^1.0.1"
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+
+aproba@^1.0.3:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1"
+
+are-we-there-yet@~1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
+arguejs@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/arguejs/-/arguejs-0.2.3.tgz#b6f939f5fe0e3cd1f3f93e2aa9262424bf312af7"
+
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+
+array-uniq@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+
+arrify@^1.0.0, arrify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+
+ascli@~1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ascli/-/ascli-1.0.1.tgz#bcfa5974a62f18e81cabaeb49732ab4a88f906bc"
+ dependencies:
+ colour "~0.7.1"
+ optjs "~3.2.2"
+
+asn1@~0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+
+assert-plus@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
+
+async@^2.0.1, async@^2.3.0, async@^2.4.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
+ dependencies:
+ lodash "^4.14.0"
+
+async@~0.9.0:
+ version "0.9.2"
+ resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
+
+async@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9"
+
+async@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.3.0.tgz#1013d1051047dd320fe24e494d5c66ecaf6147d9"
+ dependencies:
+ lodash "^4.14.0"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+aws-sign2@~0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
+
+aws4@^1.2.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
+base64url@2.0.0, base64url@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+ dependencies:
+ tweetnacl "^0.14.3"
+
+block-stream@*:
+ version "0.0.9"
+ resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+ dependencies:
+ inherits "~2.0.0"
+
+body-parser@1.17.2:
+ version "1.17.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee"
+ dependencies:
+ bytes "2.4.0"
+ content-type "~1.0.2"
+ debug "2.6.7"
+ depd "~1.1.0"
+ http-errors "~1.6.1"
+ iconv-lite "0.4.15"
+ on-finished "~2.3.0"
+ qs "6.4.0"
+ raw-body "~2.2.0"
+ type-is "~1.6.15"
+
+boom@2.x.x:
+ version "2.10.1"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
+ dependencies:
+ hoek "2.x.x"
+
+brace-expansion@^1.1.7:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+buffer-equal-constant-time@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
+
+buffer-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe"
+
+builtin-modules@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
+bytebuffer@~5:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd"
+ dependencies:
+ long "~3"
+
+bytes@2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339"
+
+camelcase@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
+
+camelcase@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+
+capture-stack-trace@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d"
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
+cli-table2@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97"
+ dependencies:
+ lodash "^3.10.1"
+ string-width "^1.0.1"
+ optionalDependencies:
+ colors "^1.1.2"
+
+cliui@^3.0.3, cliui@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wrap-ansi "^2.0.0"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+
+colors@1.0.x:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
+
+colors@1.1.2, colors@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
+
+colour@~0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778"
+
+combined-stream@^1.0.5, combined-stream@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
+ dependencies:
+ delayed-stream "~1.0.0"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
+concat-stream@^1.5.0, concat-stream@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+configstore@3.1.1, configstore@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90"
+ dependencies:
+ dot-prop "^4.1.0"
+ graceful-fs "^4.1.2"
+ make-dir "^1.0.0"
+ unique-string "^1.0.0"
+ write-file-atomic "^2.0.0"
+ xdg-basedir "^3.0.0"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+
+content-disposition@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
+
+content-type@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
+
+cookie-signature@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+
+cookie@0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
+
+core-util-is@1.0.2, core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+create-error-class@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6"
+ dependencies:
+ capture-stack-trace "^1.0.0"
+
+cross-spawn@^5.0.1:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+ dependencies:
+ lru-cache "^4.0.1"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+cryptiles@2.x.x:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
+ dependencies:
+ boom "2.x.x"
+
+crypto-random-string@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
+
+cycle@1.0.x:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ dependencies:
+ assert-plus "^1.0.0"
+
+debug@2.6.7:
+ version "2.6.7"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e"
+ dependencies:
+ ms "2.0.0"
+
+debug@2.6.8, debug@^2.2.0:
+ version "2.6.8"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
+ dependencies:
+ ms "2.0.0"
+
+decamelize@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+
+decompress-response@^3.2.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
+ dependencies:
+ mimic-response "^1.0.0"
+
+deep-equal@~0.2.1:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-0.2.2.tgz#84b745896f34c684e98f2ce0e42abaf43bba017d"
+
+deep-extend@~0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+
+depd@1.1.1, depd@~1.1.0, depd@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
+
+destroy@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+
+dot-prop@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
+ dependencies:
+ is-obj "^1.0.0"
+
+duplexer3@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
+
+duplexify@^3.1.2, duplexify@^3.5.0:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.1.tgz#4e1516be68838bc90a49994f0b39a6e5960befcd"
+ dependencies:
+ end-of-stream "^1.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+ stream-shift "^1.0.0"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
+ dependencies:
+ jsbn "~0.1.0"
+
+ecdsa-sig-formatter@1.0.9:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1"
+ dependencies:
+ base64url "^2.0.0"
+ safe-buffer "^5.0.1"
+
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+
+encodeurl@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
+
+end-of-stream@^1.0.0, end-of-stream@^1.1.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.0.tgz#7a90d833efda6cfa6eac0f4949dbb0fad3a63206"
+ dependencies:
+ once "^1.4.0"
+
+ent@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
+
+error-ex@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+ dependencies:
+ is-arrayish "^0.2.1"
+
+escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+
+etag@~1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051"
+
+eventemitter3@1.x.x:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
+
+execa@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
+ 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"
+
+express@4.15.4:
+ version "4.15.4"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.15.4.tgz#032e2253489cf8fce02666beca3d11ed7a2daed1"
+ dependencies:
+ accepts "~1.3.3"
+ array-flatten "1.1.1"
+ content-disposition "0.5.2"
+ content-type "~1.0.2"
+ cookie "0.3.1"
+ cookie-signature "1.0.6"
+ debug "2.6.8"
+ depd "~1.1.1"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ etag "~1.8.0"
+ finalhandler "~1.0.4"
+ fresh "0.5.0"
+ merge-descriptors "1.0.1"
+ methods "~1.1.2"
+ on-finished "~2.3.0"
+ parseurl "~1.3.1"
+ path-to-regexp "0.1.7"
+ proxy-addr "~1.1.5"
+ qs "6.5.0"
+ range-parser "~1.2.0"
+ send "0.15.4"
+ serve-static "1.12.4"
+ setprototypeof "1.0.3"
+ statuses "~1.3.1"
+ type-is "~1.6.15"
+ utils-merge "1.0.0"
+ vary "~1.1.1"
+
+extend@^3.0.0, extend@~3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
+
+extsprintf@1.3.0, extsprintf@^1.2.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+
+eyes@0.1.x:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
+
+fast-deep-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
+
+finalhandler@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.4.tgz#18574f2e7c4b98b8ae3b230c21f201f31bdb3fb7"
+ dependencies:
+ debug "2.6.8"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ parseurl "~1.3.1"
+ statuses "~1.3.1"
+ unpipe "~1.0.0"
+
+find-up@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ dependencies:
+ locate-path "^2.0.0"
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+
+form-data@~2.1.1:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.5"
+ mime-types "^2.1.12"
+
+forwarded@~0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363"
+
+fresh@0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
+fstream-ignore@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
+ dependencies:
+ fstream "^1.0.0"
+ inherits "2"
+ minimatch "^3.0.0"
+
+fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
+ dependencies:
+ graceful-fs "^4.1.2"
+ inherits "~2.0.0"
+ mkdirp ">=0.5 0"
+ rimraf "2"
+
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
+gcp-metadata@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-0.2.0.tgz#62dafca65f3a631bc8ce2ec3b77661f5f9387a0a"
+ dependencies:
+ extend "^3.0.0"
+ retry-request "^2.0.0"
+
+gcs-resumable-upload@^0.8.0:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/gcs-resumable-upload/-/gcs-resumable-upload-0.8.1.tgz#bb9eb7dfbacc8d77f2136b99661e693058fa3be3"
+ dependencies:
+ buffer-equal "^1.0.0"
+ configstore "^3.0.0"
+ google-auto-auth "^0.7.1"
+ pumpify "^1.3.3"
+ request "^2.81.0"
+ stream-events "^1.0.1"
+ through2 "^2.0.0"
+
+get-caller-file@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+
+get-stream@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob@^7.0.5:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ 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"
+
+google-auth-library@^0.10.0, google-auth-library@~0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.10.0.tgz#6e15babee85fd1dd14d8d128a295b6838d52136e"
+ dependencies:
+ gtoken "^1.2.1"
+ jws "^3.1.4"
+ lodash.noop "^3.0.1"
+ request "^2.74.0"
+
+google-auto-auth@^0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/google-auto-auth/-/google-auto-auth-0.7.1.tgz#c8260444912dd8ceeccd838761d56f462937bd02"
+ dependencies:
+ async "^2.3.0"
+ gcp-metadata "^0.2.0"
+ google-auth-library "^0.10.0"
+ request "^2.79.0"
+
+google-p12-pem@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-0.1.2.tgz#33c46ab021aa734fa0332b3960a9a3ffcb2f3177"
+ dependencies:
+ node-forge "^0.7.1"
+
+google-proto-files@0.12.1:
+ version "0.12.1"
+ resolved "https://registry.yarnpkg.com/google-proto-files/-/google-proto-files-0.12.1.tgz#6434dc7e025a0d0c82e5f04e615c737d6a4c4387"
+
+googleapis@20.1.0:
+ version "20.1.0"
+ resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-20.1.0.tgz#efb2541f0cab123492bc8ccfe09fa6baaf2b84ca"
+ dependencies:
+ async "~2.3.0"
+ google-auth-library "~0.10.0"
+ string-template "~1.0.0"
+
+got@7.1.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a"
+ 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"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.2:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+
+grpc@1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.4.1.tgz#3ee4a8346a613f2823928c9f8f99081b6368ec7c"
+ dependencies:
+ arguejs "^0.2.3"
+ lodash "^4.15.0"
+ nan "^2.0.0"
+ node-pre-gyp "^0.6.35"
+ protobufjs "^5.0.0"
+
+gtoken@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-1.2.2.tgz#172776a1a9d96ac09fc22a00f5be83cee6de8820"
+ dependencies:
+ google-p12-pem "^0.1.0"
+ jws "^3.0.0"
+ mime "^1.2.11"
+ request "^2.72.0"
+
+har-schema@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
+
+har-validator@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
+ dependencies:
+ ajv "^4.9.1"
+ har-schema "^1.0.5"
+
+has-symbol-support-x@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.0.tgz#442d89b1d0ac6cf5ff2f7b916ee539869b93a256"
+
+has-to-string-tag-x@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.0.tgz#49d7bcde85c2409be38ac327e3e119a451657c7b"
+ dependencies:
+ has-symbol-support-x "^1.4.0"
+
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+
+hash-stream-validation@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.1.tgz#ecc9b997b218be5bb31298628bb807869b73dcd1"
+ dependencies:
+ through2 "^2.0.0"
+
+hawk@~3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
+ dependencies:
+ boom "2.x.x"
+ cryptiles "2.x.x"
+ hoek "2.x.x"
+ sntp "1.x.x"
+
+hoek@2.x.x:
+ version "2.16.3"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
+
+hosted-git-info@^2.1.4:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c"
+
+http-errors@~1.6.1, http-errors@~1.6.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
+ dependencies:
+ depd "1.1.1"
+ inherits "2.0.3"
+ setprototypeof "1.0.3"
+ statuses ">= 1.3.1 < 2"
+
+http-proxy@1.16.2:
+ version "1.16.2"
+ resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742"
+ dependencies:
+ eventemitter3 "1.x.x"
+ requires-port "1.x.x"
+
+http-signature@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
+ dependencies:
+ assert-plus "^0.2.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+i@0.3.x:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/i/-/i-0.3.5.tgz#1d2b854158ec8169113c6cb7f6b6801e99e211d5"
+
+iconv-lite@0.4.15:
+ version "0.4.15"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+ini@~1.3.0:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
+
+invert-kv@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+
+ipaddr.js@1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+
+is-builtin-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+
+is-obj@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
+
+is-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470"
+
+is-plain-obj@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
+
+is-retry-allowed@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
+
+is-stream-ended@^0.1.0:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/is-stream-ended/-/is-stream-ended-0.1.3.tgz#a0473b267c756635486beedc7e3344e549d152ac"
+
+is-stream@^1.0.0, is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+
+is@^3.0.1, is@^3.2.0:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/is/-/is-3.2.1.tgz#d0ac2ad55eb7b0bec926a5266f6c662aaa83dca5"
+
+isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
+isstream@0.1.x, isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+
+isurl@^1.0.0-alpha5:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67"
+ dependencies:
+ has-to-string-tag-x "^1.2.0"
+ is-object "^1.0.1"
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+
+json-schema-traverse@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+
+json-stable-stringify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
+ dependencies:
+ jsonify "~0.0.0"
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+
+jsonify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+jwa@^1.1.4:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5"
+ dependencies:
+ base64url "2.0.0"
+ buffer-equal-constant-time "1.0.1"
+ ecdsa-sig-formatter "1.0.9"
+ safe-buffer "^5.0.1"
+
+jws@^3.0.0, jws@^3.1.4:
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2"
+ dependencies:
+ base64url "^2.0.0"
+ jwa "^1.1.4"
+ safe-buffer "^5.0.1"
+
+lcid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+ dependencies:
+ invert-kv "^1.0.0"
+
+load-json-file@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ strip-bom "^3.0.0"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+lodash.noop@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c"
+
+lodash@4.17.4, lodash@^4.14.0, lodash@^4.15.0:
+ version "4.17.4"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
+
+lodash@^3.10.1:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
+
+log-driver@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056"
+
+long@~3:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b"
+
+lowercase-keys@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
+
+lru-cache@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+make-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978"
+ dependencies:
+ pify "^2.3.0"
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+
+mem@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
+ dependencies:
+ mimic-fn "^1.0.0"
+
+merge-descriptors@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+
+methmeth@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/methmeth/-/methmeth-1.1.0.tgz#e80a26618e52f5c4222861bb748510bd10e29089"
+
+methods@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+
+mime-db@~1.29.0:
+ version "1.29.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878"
+
+mime-types@^2.0.8, mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7:
+ version "2.1.16"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23"
+ dependencies:
+ mime-db "~1.29.0"
+
+mime@1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
+
+mime@^1.2.11:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0"
+
+mimic-fn@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
+
+mimic-response@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e"
+
+minimatch@^3.0.0, minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+
+mkdirp@0.x.x, "mkdirp@>=0.5 0", mkdirp@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ dependencies:
+ minimist "0.0.8"
+
+modelo@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/modelo/-/modelo-4.2.0.tgz#3b4b420023a66ca7e32bdba16e710937e14d1b0b"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+mute-stream@~0.0.4:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
+
+nan@^2.0.0:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
+
+ncp@1.0.x:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ncp/-/ncp-1.0.1.tgz#d15367e5cb87432ba117d2bf80fdf45aecfb4246"
+
+negotiator@0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
+
+node-forge@^0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300"
+
+node-pre-gyp@^0.6.35:
+ version "0.6.36"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786"
+ dependencies:
+ mkdirp "^0.5.1"
+ nopt "^4.0.1"
+ npmlog "^4.0.2"
+ rc "^1.1.7"
+ request "^2.81.0"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^2.2.1"
+ tar-pack "^3.4.0"
+
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
+normalize-package-data@^2.3.2:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ dependencies:
+ path-key "^2.0.0"
+
+npmlog@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+
+oauth-sign@~0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
+
+object-assign@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+on-finished@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+ dependencies:
+ ee-first "1.1.1"
+
+once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ dependencies:
+ wrappy "1"
+
+optjs@~3.2.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/optjs/-/optjs-3.2.2.tgz#69a6ce89c442a44403141ad2f9b370bd5bb6f4ee"
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+
+os-locale@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
+ dependencies:
+ lcid "^1.0.0"
+
+os-locale@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
+ dependencies:
+ execa "^0.7.0"
+ lcid "^1.0.0"
+ mem "^1.1.0"
+
+os-tmpdir@^1.0.0, os-tmpdir@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+
+osenv@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
+p-cancelable@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+
+p-limit@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ dependencies:
+ p-limit "^1.1.0"
+
+p-timeout@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.0.tgz#9820f99434c5817868b4f34809ee5291660d5b6c"
+ dependencies:
+ p-finally "^1.0.0"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ dependencies:
+ error-ex "^1.2.0"
+
+parseurl@~1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56"
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-key@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+
+path-to-regexp@0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
+
+path-type@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+ dependencies:
+ pify "^2.0.0"
+
+performance-now@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
+
+pify@^2.0.0, pify@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+
+pkginfo@0.3.x:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21"
+
+pkginfo@0.x.x:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.0.tgz#349dbb7ffd38081fcadc0853df687f0c7744cd65"
+
+prepend-http@^1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
+
+process-nextick-args@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
+
+prompt@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/prompt/-/prompt-1.0.0.tgz#8e57123c396ab988897fb327fd3aedc3e735e4fe"
+ dependencies:
+ colors "^1.1.2"
+ pkginfo "0.x.x"
+ read "1.0.x"
+ revalidator "0.1.x"
+ utile "0.3.x"
+ winston "2.1.x"
+
+protobufjs@^5.0.0:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-5.0.2.tgz#59748d7dcf03d2db22c13da9feb024e16ab80c91"
+ dependencies:
+ ascli "~1"
+ bytebuffer "~5"
+ glob "^7.0.5"
+ yargs "^3.10.0"
+
+protochain@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/protochain/-/protochain-1.0.5.tgz#991c407e99de264aadf8f81504b5e7faf7bfa260"
+
+proxy-addr@~1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918"
+ dependencies:
+ forwarded "~0.1.0"
+ ipaddr.js "1.4.0"
+
+pseudomap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+
+pump@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.2.tgz#3b3ee6512f94f0e575538c17995f9f16990a5d51"
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+pumpify@^1.3.3:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.3.5.tgz#1b671c619940abcaeac0ad0e3a3c164be760993b"
+ dependencies:
+ duplexify "^3.1.2"
+ inherits "^2.0.1"
+ pump "^1.0.0"
+
+punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+
+qs@6.4.0, qs@~6.4.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
+
+qs@6.5.0:
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49"
+
+range-parser@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
+
+raw-body@~2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96"
+ dependencies:
+ bytes "2.4.0"
+ iconv-lite "0.4.15"
+ unpipe "1.0.0"
+
+rc@^1.1.7:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
+ dependencies:
+ deep-extend "~0.4.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+read-pkg-up@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+ dependencies:
+ find-up "^2.0.0"
+ read-pkg "^2.0.0"
+
+read-pkg@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+ dependencies:
+ load-json-file "^2.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^2.0.0"
+
+read@1.0.x:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
+ dependencies:
+ mute-stream "~0.0.4"
+
+readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.0.3"
+ util-deprecate "~1.0.1"
+
+request@^2.72.0, request@^2.74.0, request@^2.79.0, request@^2.81.0:
+ version "2.81.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
+ dependencies:
+ aws-sign2 "~0.6.0"
+ aws4 "^1.2.1"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.0"
+ forever-agent "~0.6.1"
+ form-data "~2.1.1"
+ har-validator "~4.2.1"
+ hawk "~3.1.3"
+ http-signature "~1.1.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.7"
+ oauth-sign "~0.8.1"
+ performance-now "^0.2.0"
+ qs "~6.4.0"
+ safe-buffer "^5.0.1"
+ stringstream "~0.0.4"
+ tough-cookie "~2.3.0"
+ tunnel-agent "^0.6.0"
+ uuid "^3.0.0"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+
+requires-port@1.x.x:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+
+retry-request@^2.0.0:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-2.0.5.tgz#d089a14a15db9ed60685b8602b40f4dcc0d3fb3c"
+ dependencies:
+ request "^2.81.0"
+ through2 "^2.0.0"
+
+revalidator@0.1.x:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.1.8.tgz#fece61bfa0c1b52a206bd6b18198184bdd523a3b"
+
+rimraf@2, rimraf@2.6.1, rimraf@2.x.x, rimraf@^2.5.1, rimraf@^2.6.1:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
+ dependencies:
+ glob "^7.0.5"
+
+safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+
+"semver@2 || 3 || 4 || 5", semver@5.4.1, semver@^5.3.0:
+ version "5.4.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
+
+send@0.15.4:
+ version "0.15.4"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.15.4.tgz#985faa3e284b0273c793364a35c6737bd93905b9"
+ dependencies:
+ debug "2.6.8"
+ depd "~1.1.1"
+ destroy "~1.0.4"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ etag "~1.8.0"
+ fresh "0.5.0"
+ http-errors "~1.6.2"
+ mime "1.3.4"
+ ms "2.0.0"
+ on-finished "~2.3.0"
+ range-parser "~1.2.0"
+ statuses "~1.3.1"
+
+serializerr@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/serializerr/-/serializerr-1.0.3.tgz#12d4c5aa1c3ffb8f6d1dc5f395aa9455569c3f91"
+ dependencies:
+ protochain "^1.0.5"
+
+serve-static@1.12.4:
+ version "1.12.4"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.4.tgz#9b6aa98eeb7253c4eedc4c1f6fdbca609901a961"
+ dependencies:
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ parseurl "~1.3.1"
+ send "0.15.4"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+
+setprototypeof@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
+signal-exit@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+
+slide@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
+
+sntp@1.x.x:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
+ dependencies:
+ hoek "2.x.x"
+
+spdx-correct@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
+ dependencies:
+ spdx-license-ids "^1.0.2"
+
+spdx-expression-parse@~1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c"
+
+spdx-license-ids@^1.0.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
+
+split-array-stream@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/split-array-stream/-/split-array-stream-1.0.3.tgz#d2b75a8e5e0d824d52fdec8b8225839dc2e35dfa"
+ dependencies:
+ async "^2.4.0"
+ is-stream-ended "^0.1.0"
+
+sshpk@^1.7.0:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ dashdash "^1.12.0"
+ getpass "^0.1.1"
+ optionalDependencies:
+ bcrypt-pbkdf "^1.0.0"
+ ecc-jsbn "~0.1.1"
+ jsbn "~0.1.0"
+ tweetnacl "~0.14.0"
+
+stack-trace@0.0.x:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
+
+"statuses@>= 1.3.1 < 2", statuses@~1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
+
+stream-events@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.2.tgz#abf39f66c0890a4eb795bc8d5e859b2615b590b2"
+ dependencies:
+ stubs "^3.0.0"
+
+stream-shift@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
+
+string-format-obj@^1.0.0, string-format-obj@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/string-format-obj/-/string-format-obj-1.1.0.tgz#7635610b1ef397013e8478be98a170e04983d068"
+
+string-template@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
+
+string-width@^1.0.1, string-width@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+string-width@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string_decoder@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
+ dependencies:
+ safe-buffer "~5.1.0"
+
+stringstream@~0.0.4:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+
+strip-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+
+stubs@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b"
+
+tar-pack@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984"
+ dependencies:
+ debug "^2.2.0"
+ fstream "^1.0.10"
+ fstream-ignore "^1.0.5"
+ once "^1.3.3"
+ readable-stream "^2.1.4"
+ rimraf "^2.5.1"
+ tar "^2.2.1"
+ uid-number "^0.0.6"
+
+tar@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
+ dependencies:
+ block-stream "*"
+ fstream "^1.0.2"
+ inherits "2"
+
+through2@^2.0.0, through2@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
+ dependencies:
+ readable-stream "^2.1.5"
+ xtend "~4.0.1"
+
+timed-out@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
+
+tmp@0.0.31:
+ version "0.0.31"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
+ dependencies:
+ os-tmpdir "~1.0.1"
+
+tough-cookie@~2.3.0:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a"
+ dependencies:
+ punycode "^1.4.1"
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+
+type-is@~1.6.15:
+ version "1.6.15"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.15"
+
+typedarray@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+
+uid-number@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
+
+unique-string@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a"
+ dependencies:
+ crypto-random-string "^1.0.0"
+
+unpipe@1.0.0, unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+
+url-parse-lax@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73"
+ dependencies:
+ prepend-http "^1.0.1"
+
+url-to-options@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
+
+util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+
+utile@0.3.x:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/utile/-/utile-0.3.0.tgz#1352c340eb820e4d8ddba039a4fbfaa32ed4ef3a"
+ dependencies:
+ async "~0.9.0"
+ deep-equal "~0.2.1"
+ i "0.3.x"
+ mkdirp "0.x.x"
+ ncp "1.0.x"
+ rimraf "2.x.x"
+
+utils-merge@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8"
+
+uuid@3.1.0, uuid@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
+ dependencies:
+ spdx-correct "~1.0.0"
+ spdx-expression-parse "~1.0.0"
+
+vary@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37"
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+
+which@^1.2.9:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
+ dependencies:
+ string-width "^1.0.2"
+
+window-size@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876"
+
+winston@2.1.x:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/winston/-/winston-2.1.1.tgz#3c9349d196207fd1bdff9d4bc43ef72510e3a12e"
+ dependencies:
+ async "~1.0.0"
+ colors "1.0.x"
+ cycle "1.0.x"
+ eyes "0.1.x"
+ isstream "0.1.x"
+ pkginfo "0.3.x"
+ stack-trace "0.0.x"
+
+winston@2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/winston/-/winston-2.3.1.tgz#0b48420d978c01804cf0230b648861598225a119"
+ dependencies:
+ async "~1.0.0"
+ colors "1.0.x"
+ cycle "1.0.x"
+ eyes "0.1.x"
+ isstream "0.1.x"
+ stack-trace "0.0.x"
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+write-file-atomic@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.1.0.tgz#1769f4b551eedce419f0505deae2e26763542d37"
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ slide "^1.1.5"
+
+xdg-basedir@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
+
+xtend@~4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+
+y18n@^3.2.0, y18n@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+
+yallist@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+
+yargs-parser@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
+ dependencies:
+ camelcase "^4.1.0"
+
+yargs@8.0.2:
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"
+ dependencies:
+ camelcase "^4.1.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^2.0.0"
+ read-pkg-up "^2.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^7.0.0"
+
+yargs@^3.10.0:
+ version "3.32.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995"
+ dependencies:
+ camelcase "^2.0.1"
+ cliui "^3.0.3"
+ decamelize "^1.1.1"
+ os-locale "^1.4.0"
+ string-width "^1.0.1"
+ window-size "^0.1.4"
+ y18n "^3.2.0"
diff --git a/src/server_manager/bower.json b/src/server_manager/bower.json
new file mode 100644
index 000000000..9f5ac8814
--- /dev/null
+++ b/src/server_manager/bower.json
@@ -0,0 +1,37 @@
+{
+ "name": "outline-launcher",
+ "description": "Launches Outline Shadowsocks servers on the cloud",
+ "main": "",
+ "authors": [
+ "Outline"
+ ],
+ "license": "Apache",
+ "private": true,
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "test",
+ "tests"
+ ],
+ "dependencies": {
+ "app-layout": "PolymerElements/app-layout#^2.0.4",
+ "iron-autogrow-textarea": "PolymerElements/iron-autogrow-textarea#^2.1.1",
+ "iron-icons": "PolymerElements/iron-icons#^2.0.1",
+ "iron-fit-behavior": "PolymerElements/iron-fit-behavior#^2.1.0",
+ "iron-pages": "PolymerElements/iron-pages#^2.0.1",
+ "paper-button": "PolymerElements/paper-button#^2.0.0",
+ "paper-dialog": "PolymerElements/paper-dialog#^2.0.1",
+ "paper-dialog-scrollable": "PolymerElements/paper-dialog-scrollable#^2.1.0",
+ "paper-dropdown-menu": "PolymerElements/paper-dropdown-menu#^2.0.2",
+ "paper-icon-button": "PolymerElements/paper-icon-button#^2.0.1",
+ "paper-input": "PolymerElements/paper-input#^2.1.0",
+ "paper-item": "PolymerElements/paper-item#^2.0.0",
+ "paper-listbox": "PolymerElements/paper-listbox#^2.0.0",
+ "paper-progress": "PolymerElements/paper-progress#^2.0.1",
+ "paper-toast": "PolymerElements/paper-toast#^2.0.0",
+ "paper-toggle-button": "PolymerElements/paper-toggle-button#^2.0.0",
+ "web-animations-js": "web-animations/web-animations-js",
+ "webcomponentsjs": "webcomponents/webcomponentsjs#^v1.1.0"
+ }
+}
diff --git a/src/server_manager/cloud/digitalocean_api.ts b/src/server_manager/cloud/digitalocean_api.ts
new file mode 100644
index 000000000..2c4ad2973
--- /dev/null
+++ b/src/server_manager/cloud/digitalocean_api.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 * as events from 'events';
+
+import * as errors from '../infrastructure/errors';
+
+export interface DigitalOceanDropletSpecification {
+ installCommand: string;
+ size: string;
+ image: string;
+ tags: string[];
+}
+
+// Returns an OAuth redirect URL for DigitalOcean.
+export function getOauthUrl(clientId: string, redirectUri: string, state?: string): string {
+ return 'https://cloud.digitalocean.com/v1/oauth/authorize?' +
+ 'client_id=' + clientId + '&' +
+ 'response_type=token&' +
+ 'redirect_uri=' + encodeURIComponent(redirectUri) + '&' +
+ 'state=' + encodeURIComponent(state || '') + '&' +
+ 'scope=read%20write';
+}
+
+// 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 < {
+ email: string;
+ uuid: string;
+ email_verified: boolean;
+ status: 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.OutlineError {
+ 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 function createDigitalOceanSession(accessToken: string): DigitalOceanSession {
+ return new RestApiSession(accessToken);
+}
+
+class RestApiSession implements DigitalOceanSession {
+ // Constructor takes a DigitalOcean access token, which should have
+ // read+write permissions.
+ constructor(public accessToken: string) {}
+
+ public getAccount(): Promise {
+ 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.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,
+ });
+ });
+ }
+
+ public deleteDroplet(dropletId: number): Promise {
+ return this.request('DELETE', 'droplets/' + dropletId);
+ }
+
+ public getRegionInfo(): Promise {
+ 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 {
+ 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 {
+ 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 {
+ return this.request<{droplets: DropletInfo[]}>('GET', `droplets/?tag_name=${encodeURI(tag)}`)
+ .then((response) => {
+ return response.droplets;
+ });
+ }
+
+ public getDroplets(): Promise {
+ return this.request<{droplets: DropletInfo[]}>('GET', 'droplets/').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 {
+ // this.response is a JSON object, whose message is an error string.
+ const responseJson = JSON.parse(xhr.response);
+ 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).
+ 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/electron_app/build_action.sh b/src/server_manager/electron_app/build_action.sh
new file mode 100755
index 000000000..146d43887
--- /dev/null
+++ b/src/server_manager/electron_app/build_action.sh
@@ -0,0 +1,53 @@
+#!/bin/bash -eux
+#
+# 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.
+
+# Builds the Electron App
+
+readonly OUT_DIR=$BUILD_DIR/server_manager/electron_app
+rm -rf $OUT_DIR
+
+readonly NODE_MODULES_BIN_DIR=$ROOT_DIR/src/server_manager/node_modules/.bin
+
+# Build the Web App.
+do_action server_manager/web_app/build
+
+# Compile the Electron app source.
+# Since Node.js on Cygwin doesn't like absolute Unix-style paths,
+# we'll use relative paths here.
+tsc -p src/server_manager/electron_app/tsconfig.json --outDir build/server_manager/electron_app/js
+
+# Assemble everything together.
+readonly MODULE_DIR=$(dirname $0)
+readonly STATIC_DIR=$OUT_DIR/static
+mkdir -p $STATIC_DIR
+mkdir -p $STATIC_DIR/server_manager
+cp -r $OUT_DIR/js/* $STATIC_DIR
+cp -r $BUILD_DIR/server_manager/web_app/static $STATIC_DIR/server_manager/web_app/
+cp $MODULE_DIR/config.json $STATIC_DIR
+# Our electron app assumes all HTML files will be in the web_app directory.
+cp $MODULE_DIR/loading.html $STATIC_DIR/server_manager/web_app/
+
+# Electron requires a package.json file for the app's name, etc.
+# We also need to install NPMs at this location for require()
+# in order for require() to work right in the renderer process, which
+# is loaded via a custom protocol.
+cp src/server_manager/package.json yarn.lock $STATIC_DIR
+cd $STATIC_DIR
+yarn install --prod --ignore-scripts
+
+# Icons.
+cd $ROOT_DIR
+$NODE_MODULES_BIN_DIR/electron-icon-maker --input=src/server_manager/images/launcher-icon.png --output=build/server_manager/electron_app/static
diff --git a/src/server_manager/electron_app/config.json b/src/server_manager/electron_app/config.json
new file mode 100644
index 000000000..98997caa0
--- /dev/null
+++ b/src/server_manager/electron_app/config.json
@@ -0,0 +1,4 @@
+{
+ "version": "1.0.0",
+ "releaseDataUrl": "https://raw.githubusercontent.com/Jigsaw-Code/outline-server/master/release/release_data.json"
+}
diff --git a/src/server_manager/electron_app/digital_ocean_modifications.ts b/src/server_manager/electron_app/digital_ocean_modifications.ts
new file mode 100644
index 000000000..f05d326d6
--- /dev/null
+++ b/src/server_manager/electron_app/digital_ocean_modifications.ts
@@ -0,0 +1,476 @@
+// 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 electron from 'electron';
+
+const ipcRenderer = electron.ipcRenderer;
+
+export function modifyUiIfDigitalOcean() {
+ // Wait for load event, to ensure that the currentUser object is loaded.
+ // For most signed-in DigitalOcean pages, currentUser is set via inline
+ //
+
+ Loading...
+
+