diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0e1e35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +build/ + +gui/public/bundle.js +gui/public/main.css \ No newline at end of file diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..29fa3fa --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +v9.2.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..9077918 --- /dev/null +++ b/README.md @@ -0,0 +1,235 @@ +# SSHmon + +SSHmon is a program designed to manage and monitor ssh connections. +Still at an early stage of development, it has been tested on Linux and OSX with SSH ≥ 6.7. + +![gui-sample.png](https://s7.postimg.cc/elzqf92gp/gui-sample.png) + +## How it works + +SSHmon builds on top of the SSH "Control Master" feature, that facilitates port forwarding setup. + +## Disclaimer + +SSHmon Features (e.g. SSH host definition and connection) are exposed through the GUI. As a consequence, extreme care should be taken to make sure it is only reachable by you. Use at your own risk. + +## Features + +- Nice GUI +- SSH port/socket forwarding management +- Configuration with YAML file +- Automatic start and retry of connection and port forwarding +- HTTP forwarding + +## Get started + +Get the [latest release](https://github.com/hpello/sshmon/releases/latest) of SSHmon for your system. + +Or, build it from source. + +- Install: + ```bash + npm run install-all + npm run deploy + ``` + +- Run: + ```bash + build/sshmon -h + ``` + +And access the GUI at . + +See [logging](#logging) for output options. + +## Configure + +You can set up SSH connections through the GUI or with a configuration file. + +By default, sshmon will create a config file located at `~/.sshmon/config.yml`. +You also may specify your own configuration file. + +### Hosts + +SSHmon is not a replacement of your SSH config! + +Actually, it is recommended you set up first your host in your SSH config file (by default `~/.ssh/config`), before adding it to your SSHmon config. + +- Syntax + + ```yaml + hosts: + host-id-1: + # host config + host-id-2: + # host config + ... + ``` + +- List of available options + + | Option | Type | Description | Required | Default | + | ------ | ---- | ----------- | -------- | ------- | + | label | string | Friendly name for the GUI | no | '' | + | ssh.host | string | Host passed to ssh | no | host id | + | ssh.config | object | Options passed to ssh as `-o key=value` | no | {} | + | autostart | boolean | Try to connect to host at SSHmon startup | no | false | + | autoretry | boolean | Try to reconnect to host on connection error | no | false | + | forward | object | Forwardings for this host | no | {} | + +### Forwardings + +SSHmon allows you to define port/socket forwarding on your SSH connections. Here are the possible forwardings: + +| Type | Bind | Target | +| ---- | ---- | ------ | +| Local | `[address:]port` or unix socket (local) | `[address:]port` or unix socket (remote) | +| Remote | `[address:]port` or unix socket (remote) | `[address:]port` or unix socket (local) | +| Dynamic | `[address:]port` (local) | | +| HTTP | | `[address:]port` or unix socket (remote) | + +Please read the SSH documentation for local, remote and dynamic types. + +#### HTTP forwarding + +The HTTP forwarding type is specific to SSHmon. +It establishes a local port forwarding to a unix socket managed by SSHmon, and allows access to the remote port/socket through the GUI. + +It was designed to allow easy access to a remote running SSHmon instance, but should also work for other HTTP services that can be mounted under an arbitrary HTTP path prefix. + +#### Default address values + +- For the `bind` parameter, if you do not specify an address, SSH has its own policy for the default interface it will bind to. +- For the `target` parameter, SSH normally requires you to specify an interface. With SSHmon you may specify a single port value, the default interface being `localhost`. + +#### Forwarding config + +- Syntax + + ```yaml + # inside host config: + forward: + forward-1: + # forwarding config + forward-2: + # forwarding config + ``` + +- List of available options for a forwarding + + | Option | Type | Description | Required | Default | + | ------ | ---- | ----------- | -------- | ------- | + | label | string | Friendly name for the GUI | no | '' | + | spec | string | Host passed to ssh | yes | | + | autostart | boolean | Try to forward at host connection | no | false | + | autoretry | boolean | Retry to forward on error | no | false | + +- Spec + + Similarly to the SSH forwarding syntax, the spec syntax is: + ```bash + Letter [bind] [target] + ``` + + Where the options are required following the given table: + + | Type | Letter | Bind | Target | + | ---- | ------ | ---- | ------ | + | Local | `L` | ✓ | ✓ | + | Remote | `R` | ✓ | ✓ | + | Dynamic | `D` | ✓ | | + | HTTP | `H` | | ✓ | + +- Forwarding shorthand syntax + + You may replace the whole options object with the single spec string, e.g.: + + ```yaml + forward: + forward-1: spec1 + forward-2: spec2 + ``` + +### Config + +- Syntax + + ```yaml + config: + autosave: true + ``` + +- List of available options for config + + | Option | Type | Description | Required | Default | + | ------ | ---- | ----------- | -------- | ------- | + | autosave | boolean | Write to this file on config change | no | false | + +### Configuration file example + +```yaml +hosts: + + host-1: + label: My Favourite Host + ssh: + host: my.host.com + config: + ServerAliveInterval: 5 + ServerAliveCountMax: 3 + autostart: true + autoretry: true + forward: + forwarding-1: + label: My TCP service + spec: L 1234 localhost:80 + autostart: true + autoretry: true + forwarding-2: R 8022 22 # forwarding shorthand + sshmon: H 8377 + + host-2: # host shorthand + +config: + + autosave: true +``` + +## Logging + +Logging is done using the [bunyan](https://github.com/trentm/node-bunyan) library. For pretty format, pipe `sshmon` output to `bunyan`, e.g.: + +```bash +build/sshmon | server/node_modules/.bin/bunyan +``` + +Tip: You can set `NODE_ENV=production` for more concise logs. + +## Troubleshooting + +- So far, only public/private key authentication is supported. +- Before trying to connect to a host through SSHmon, make sure you can connect to it with SSH on the command line. + +## Built with + +SSHmon was developped thanks to the following projects (this list is not exhaustive!): + +- [Typescript](https://www.typescriptlang.org/) +- [TSLint](https://palantir.github.io/tslint/) +- [React.js](https://reactjs.org/) +- [Redux](https://redux.js.org/) +- [Bulma](https://bulma.io/) +- [Bulmaswatch](https://jenil.github.io/bulmaswatch/) +- [Socket.io](https://socket.io/) +- [Pkg](https://github.com/zeit/pkg) + +## TODO + +- Add a test suite +- Add a tutorial +- Distribute releases +- Allow to change GUI address +- Offer multiple GUI themes +- Allow custom global SSH config options +- Use BatchMode for `ProxyJump` SSH hosts +- Improve logging diff --git a/gui/package-lock.json b/gui/package-lock.json new file mode 100644 index 0000000..f22dffd --- /dev/null +++ b/gui/package-lock.json @@ -0,0 +1,5002 @@ +{ + "name": "sshmon-gui", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@browserify/acorn5-object-spread": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@browserify/acorn5-object-spread/-/acorn5-object-spread-5.0.1.tgz", + "integrity": "sha512-sFCUPzgeEjdq3rinwy4TFXtak2YZdhqpj6MdNusxkdTFr9TXAUEYK4YQSamR8Joqt/yii1drgl5hk8q/AtJDKA==", + "dev": true, + "requires": { + "acorn": "5.4.1" + }, + "dependencies": { + "acorn": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.4.1.tgz", + "integrity": "sha512-XLmq3H/BVvW6/GbxKryGxWORz1ebilSsUDlyC27bXhWGWAZWkGwS6FLHjOlwFXNFoWFQEO/Df4u0YYd0K3BQgQ==", + "dev": true + } + } + }, + "@types/node": { + "version": "8.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.1.tgz", + "integrity": "sha512-4JFGIC1RSoFngVsT5EZcL793/uRi/OJ3ilsp9DQUr4LZOaMhNM1pPrt9TqlXOnXj3h73hl6NF31v87eQAPXYTg==", + "dev": true + }, + "@types/react": { + "version": "16.0.36", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.0.36.tgz", + "integrity": "sha512-q33EVfy4i+fwhM31PL6/c6Job/DyjOiExHuR163bJK3rEMRf2ENkBrN4thQH5cwA+TiiN1vWDZU6D5H1AvQTlA==", + "dev": true + }, + "@types/react-dom": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.0.3.tgz", + "integrity": "sha512-xAvZiGhQlEhjStoKktoai8CelXVFBaSN6JX4vy1UQioRba3c2vum1TGzR0thHoEauZtIwzWg8mos0AHu2ne4jw==", + "dev": true, + "requires": { + "@types/node": "8.9.1", + "@types/react": "16.0.36" + } + }, + "@types/react-redux": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-5.0.14.tgz", + "integrity": "sha512-qw2NjfDEiqWGpOTYoS55HBCdvhfC2jb/cMs0OSIDK98Uv9z2v0EDstbTlR8wiRLpbL0d3DPz2B6wxVhQ8cPw3g==", + "dev": true, + "requires": { + "@types/react": "16.0.36", + "redux": "3.7.2" + } + }, + "@types/redux-logger": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.5.tgz", + "integrity": "sha512-9pgzzRGW8ul5+d9CXCy83OwHNcEJwY1tb8Nv3MW8LZQWfDKtildzlCXIUmaMrEmCUFfmxURV1XY0a8QIKO6HOQ==", + "dev": true, + "requires": { + "redux": "3.7.2" + } + }, + "@types/socket.io-client": { + "version": "1.4.32", + "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.32.tgz", + "integrity": "sha512-Vs55Kq8F+OWvy1RLA31rT+cAyemzgm0EWNeax6BWF8H7QiiOYMJIdcwSDdm5LVgfEkoepsWkS+40+WNb7BUMbg==", + "dev": true + }, + "JSONStream": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz", + "integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=", + "dev": true, + "requires": { + "jsonparse": "1.3.1", + "through": "2.3.8" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "dev": true + }, + "acorn-node": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.3.0.tgz", + "integrity": "sha512-efP54n3d1aLfjL2UMdaXa6DsswwzJeI5rqhbFvXMrKiJ6eJFpf+7R0zN7t8IC+XKn2YOAFAv6xbBNgHUkoHWLw==", + "dev": true, + "requires": { + "acorn": "5.4.1", + "xtend": "4.0.1" + }, + "dependencies": { + "acorn": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.4.1.tgz", + "integrity": "sha512-XLmq3H/BVvW6/GbxKryGxWORz1ebilSsUDlyC27bXhWGWAZWkGwS6FLHjOlwFXNFoWFQEO/Df4u0YYd0K3BQgQ==", + "dev": true + } + } + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "dev": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.3" + } + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "asn1.js": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.2.tgz", + "integrity": "sha512-b/OsSjvWEo8Pi8H0zsDd2P6Uqo2TK2pH8gNLSJtNLM2Db0v2QaAZ0pBQJXVjAn4gBuugeVDr7s63ZogpUIwWDg==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + } + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true + }, + "astw": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/astw/-/astw-2.2.0.tgz", + "integrity": "sha1-e9QXhNMkk5h66yOba04cV6hzuRc=", + "dev": true, + "requires": { + "acorn": "4.0.13" + } + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "dev": true + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "autoprefixer": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.2.5.tgz", + "integrity": "sha512-XqHfo8Ht0VU+T5P+eWEVoXza456KJ4l62BPewu3vpNf3LP9s2+zYXkXBznzYby4XeECXgG3N4i+hGvOhXErZmA==", + "dev": true, + "requires": { + "browserslist": "2.11.3", + "caniuse-lite": "1.0.30000805", + "normalize-range": "0.1.2", + "num2fraction": "1.2.2", + "postcss": "6.0.17", + "postcss-value-parser": "3.3.0" + } + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, + "base64-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", + "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "dev": true + }, + "blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=" + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.9.tgz", + "integrity": "sha512-/+o3o6OV1cm3WKrO7U4wykU+ZICE6HiMEuravc2d03NIuM/VaRn5iMcoQ7NyxFXjvpmRICP2EER0YOnh4yIapA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-pack": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.0.4.tgz", + "integrity": "sha512-Q4Rvn7P6ObyWfc4stqLWHtG1MJ8vVtjgT24Zbu+8UTzxYuZouqZsmNRRTFVMY/Ux0eIKv1d+JWzsInTX+fdHPQ==", + "dev": true, + "requires": { + "JSONStream": "1.3.2", + "combine-source-map": "0.8.0", + "defined": "1.0.0", + "safe-buffer": "5.1.1", + "through2": "2.0.3", + "umd": "3.0.1" + } + }, + "browser-resolve": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", + "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "browserify": { + "version": "14.5.0", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-14.5.0.tgz", + "integrity": "sha512-gKfOsNQv/toWz+60nSPfYzuwSEdzvV2WdxrVPUbPD/qui44rAkB3t3muNtmmGYHqrG56FGwX9SUEQmzNLAeS7g==", + "dev": true, + "requires": { + "JSONStream": "1.3.2", + "assert": "1.4.1", + "browser-pack": "6.0.4", + "browser-resolve": "1.11.2", + "browserify-zlib": "0.2.0", + "buffer": "5.0.8", + "cached-path-relative": "1.0.1", + "concat-stream": "1.5.2", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.12.0", + "defined": "1.0.0", + "deps-sort": "2.0.0", + "domain-browser": "1.1.7", + "duplexer2": "0.1.4", + "events": "1.1.1", + "glob": "7.1.2", + "has": "1.0.1", + "htmlescape": "1.1.1", + "https-browserify": "1.0.0", + "inherits": "2.0.3", + "insert-module-globals": "7.0.1", + "labeled-stream-splicer": "2.0.0", + "module-deps": "4.1.1", + "os-browserify": "0.3.0", + "parents": "1.0.1", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "read-only-stream": "2.0.0", + "readable-stream": "2.3.3", + "resolve": "1.5.0", + "shasum": "1.0.2", + "shell-quote": "1.6.1", + "stream-browserify": "2.0.1", + "stream-http": "2.8.0", + "string_decoder": "1.0.3", + "subarg": "1.0.0", + "syntax-error": "1.4.0", + "through2": "2.0.3", + "timers-browserify": "1.4.2", + "tty-browserify": "0.0.1", + "url": "0.11.0", + "util": "0.10.3", + "vm-browserify": "0.0.4", + "xtend": "4.0.1" + } + }, + "browserify-aes": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.1.tgz", + "integrity": "sha512-UGnTYAnB2a3YuYKIRy1/4FB2HdM866E0qC46JXvVTYKlBlZlnvfpSfY6OKfXZAkv70eJ2a1SqzpAo5CRhZGDFg==", + "dev": true, + "requires": { + "buffer-xor": "1.0.3", + "cipher-base": "1.0.4", + "create-hash": "1.1.3", + "evp_bytestokey": "1.0.3", + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + }, + "browserify-cipher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", + "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", + "dev": true, + "requires": { + "browserify-aes": "1.1.1", + "browserify-des": "1.0.0", + "evp_bytestokey": "1.0.3" + } + }, + "browserify-des": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", + "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "des.js": "1.0.0", + "inherits": "2.0.3" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "randombytes": "2.0.6" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "elliptic": "6.4.0", + "inherits": "2.0.3", + "parse-asn1": "5.1.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "1.0.6" + } + }, + "browserslist": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", + "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", + "dev": true, + "requires": { + "caniuse-lite": "1.0.30000805", + "electron-to-chromium": "1.3.33" + } + }, + "buffer": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.0.8.tgz", + "integrity": "sha512-xXvjQhVNz50v2nPeoOsNqWCLGfiv4ji/gXZM28jnVwdLJxH4mFyqgqCKfaK9zf1KUbG6zTkjLOy7ou+jSMarGA==", + "dev": true, + "requires": { + "base64-js": "1.2.1", + "ieee754": "1.1.8" + } + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bulma": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.6.2.tgz", + "integrity": "sha1-9LHRHVrMUaeWROsKKwsQZJ09cfU=" + }, + "bulmaswatch": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/bulmaswatch/-/bulmaswatch-0.6.2.tgz", + "integrity": "sha512-IMYjMqEnHQ1Yhtzkia3ojeFRvJr8DnLBiDMUuvLcGtJwyaAuZ8AQ4tT3TK21lcnt7cngeM3CKzBtkbQulWQI9w==", + "dev": true + }, + "cached-path-relative": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz", + "integrity": "sha1-0JxLUoAKpMB44t2BqGmqyQ0uVOc=", + "dev": true + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + } + }, + "caniuse-lite": { + "version": "1.0.30000805", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000805.tgz", + "integrity": "sha512-g04TTapYF47M05D8dshTSTfuAHTaAyfYUsD926QTcNvnqitFJb277L3y3RdDbcrjxmqzJBEqorkc0AykqMv8Ig==", + "dev": true + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "dev": true + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + }, + "dependencies": { + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "fsevents": "1.2.3", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + } + }, + "chokidar-cli": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-1.2.0.tgz", + "integrity": "sha1-jn9YRCJzGCAYvhho5Twir2WiGUg=", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "bluebird": "2.11.0", + "chokidar": "1.7.0", + "lodash": "3.10.1", + "shell-quote": "1.6.1", + "yargs": "3.32.0" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + } + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "2.0.0" + } + }, + "cli-spinners": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.1.0.tgz", + "integrity": "sha1-8YR7FohE2RemceudFH499JfJDQY=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combine-source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", + "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", + "dev": true, + "requires": { + "convert-source-map": "1.1.3", + "inline-source-map": "0.6.2", + "lodash.memoize": "3.0.4", + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==", + "dev": true + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.0.6", + "typedarray": "0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "0.1.4" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + }, + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cosmiconfig": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.2.2.tgz", + "integrity": "sha512-GiNXLwAFPYHy25XmTPpafYvn3CLAkJ8FLsscq78MQd1Kh0OU6Yzhn4eV2MVF4G9WEQZoWEGltatdR+ntGPMl5A==", + "dev": true, + "requires": { + "is-directory": "0.3.1", + "js-yaml": "3.10.0", + "minimist": "1.2.0", + "object-assign": "4.1.1", + "os-homedir": "1.0.2", + "parse-json": "2.2.0", + "require-from-string": "1.2.1" + } + }, + "create-ecdh": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", + "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "elliptic": "6.4.0" + } + }, + "create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "inherits": "2.0.3", + "ripemd160": "2.0.1", + "sha.js": "2.4.10" + } + }, + "create-hmac": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", + "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "create-hash": "1.1.3", + "inherits": "2.0.3", + "ripemd160": "2.0.1", + "safe-buffer": "5.1.1", + "sha.js": "2.4.10" + } + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "which": "1.3.0" + } + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.10.1" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "1.0.0", + "browserify-sign": "4.0.4", + "create-ecdh": "4.0.0", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "diffie-hellman": "5.0.2", + "inherits": "2.0.3", + "pbkdf2": "3.0.14", + "public-encrypt": "4.0.0", + "randombytes": "2.0.6", + "randomfill": "1.0.3" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-diff": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz", + "integrity": "sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ=" + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "dependency-graph": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.5.2.tgz", + "integrity": "sha512-fuF8ISen2Rk75wQ4tWHcfJ/IV1cmPMFE+wth10tAnj/JkpqMJzNW5oKpVOCAkpdfYD+NwtA3kL3nOeyprxPG2A==", + "dev": true + }, + "deps-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz", + "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=", + "dev": true, + "requires": { + "JSONStream": "1.3.2", + "shasum": "1.0.2", + "subarg": "1.0.0", + "through2": "2.0.3" + } + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "detective": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-4.7.1.tgz", + "integrity": "sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==", + "dev": true, + "requires": { + "acorn": "5.4.1", + "defined": "1.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.4.1.tgz", + "integrity": "sha512-XLmq3H/BVvW6/GbxKryGxWORz1ebilSsUDlyC27bXhWGWAZWkGwS6FLHjOlwFXNFoWFQEO/Df4u0YYd0K3BQgQ==", + "dev": true + } + } + }, + "diffie-hellman": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", + "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "miller-rabin": "4.0.1", + "randombytes": "2.0.6" + } + }, + "domain-browser": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", + "dev": true + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true, + "requires": { + "readable-stream": "2.3.3" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "electron-to-chromium": { + "version": "1.3.33", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.33.tgz", + "integrity": "sha1-vwBwPWKnxlI4E2V4w1LWxcBCpUU=", + "dev": true + }, + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0", + "hash.js": "1.1.3", + "hmac-drbg": "1.0.1", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "0.4.19" + } + }, + "engine.io-client": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.1.4.tgz", + "integrity": "sha1-T88TcLRxY70s6b4nM5ckMDUNTqE=", + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "2.6.9", + "engine.io-parser": "2.1.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "3.3.3", + "xmlhttprequest-ssl": "1.5.5", + "yeast": "0.1.2" + } + }, + "engine.io-parser": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.2.tgz", + "integrity": "sha512-dInLFzr80RijZ1rGpx1+56/uFoH7/7InhH3kZt+Ms6hT8tNx3NGW/WNSA/f8As1WkOfkuyb3tnRyuXGxusclMw==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.4", + "has-binary2": "1.0.2" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "1.3.4", + "safe-buffer": "5.1.1" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "shebang-command": "1.2.0", + "which": "1.3.0" + } + } + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.3" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fbjs": { + "version": "0.8.16", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz", + "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=", + "requires": { + "core-js": "1.2.7", + "isomorphic-fetch": "2.2.1", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "promise": "7.3.1", + "setimmediate": "1.0.5", + "ua-parser-js": "0.7.17" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.3.tgz", + "integrity": "sha512-X+57O5YkDTiEQGiw8i7wYc2nQgweIekqkepI8Q3y4wVlurgBt2SuwxTeYUYMZIGpLZH3r/TsMjczCMXE5ZOt7Q==", + "dev": true, + "optional": true, + "requires": { + "nan": "2.10.0", + "node-pre-gyp": "0.9.1" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": "2.1.2" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.9", + "iconv-lite": "0.4.21", + "sax": "1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.9.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "1.0.3", + "mkdirp": "0.5.1", + "needle": "2.2.0", + "nopt": "4.0.1", + "npm-packlist": "1.1.10", + "npmlog": "4.1.2", + "rc": "1.2.6", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "4.4.1" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.5" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "3.0.1", + "npm-bundled": "1.0.3" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.2.4", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true, + "dev": true + } + } + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "gaze": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz", + "integrity": "sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=", + "dev": true, + "requires": { + "globule": "1.2.0" + } + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "1.0.2" + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "globule": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz", + "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=", + "dev": true, + "requires": { + "glob": "7.1.2", + "lodash": "4.17.5", + "minimatch": "3.0.4" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "commander": "2.14.1", + "is-my-json-valid": "2.17.1", + "pinkie-promise": "2.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "has": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "dev": true, + "requires": { + "function-bind": "1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-binary2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.2.tgz", + "integrity": "sha1-6D26SfC5vk0CbSc2U1DZ8D9Uvpg=", + "requires": { + "isarray": "2.0.1" + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "1.1.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "hoist-non-react-statics": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz", + "integrity": "sha1-ND24TGAYxlB3iJgkATWhQg7iLOA=" + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", + "dev": true + }, + "htmlescape": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", + "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", + "dev": true + }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "inline-source-map": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "dev": true, + "requires": { + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "insert-module-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.0.1.tgz", + "integrity": "sha1-wDv04BywhtW15azorQr+eInWOMM=", + "dev": true, + "requires": { + "JSONStream": "1.3.2", + "combine-source-map": "0.7.2", + "concat-stream": "1.5.2", + "is-buffer": "1.1.6", + "lexical-scope": "1.2.0", + "process": "0.11.10", + "through2": "2.0.3", + "xtend": "4.0.1" + }, + "dependencies": { + "combine-source-map": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.7.2.tgz", + "integrity": "sha1-CHAxKFazB6h8xKxIbzqaYq7MwJ4=", + "dev": true, + "requires": { + "convert-source-map": "1.1.3", + "inline-source-map": "0.6.2", + "lodash.memoize": "3.0.4", + "source-map": "0.5.7" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "invariant": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "1.11.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-my-json-valid": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.1.tgz", + "integrity": "sha512-Q2khNw+oBlWuaYvEEHtKSw/pCxD2L5Rc1C+UQme9X6JdRDh7m5D7HkozA0qa3DUkQ6VzCnEm8mVIQPyIRkI5sQ==", + "dev": true, + "requires": { + "generate-function": "2.0.0", + "generate-object-property": "1.2.0", + "jsonpointer": "4.0.1", + "xtend": "4.0.1" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "1.7.3", + "whatwg-fetch": "2.0.3" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "js-base64": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.3.tgz", + "integrity": "sha512-H7ErYLM34CvDMto3GbD6xD0JLUGYXR3QTcH6B/tr4Hi/QpSThnCsIp+Sy5FRTw3B0d6py4HcNkW7nO/wdtGWEw==", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-stable-stringify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", + "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", + "dev": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "labeled-stream-splicer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz", + "integrity": "sha1-pS4dE4AkwAuGscDJH2d5GLiuClk=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "isarray": "0.0.1", + "stream-splicer": "2.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "lexical-scope": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/lexical-scope/-/lexical-scope-1.2.0.tgz", + "integrity": "sha1-/Ope3HBKSzqHls3KQZw6CvryLfQ=", + "dev": true, + "requires": { + "astw": "2.2.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + }, + "lodash-es": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.5.tgz", + "integrity": "sha512-Ez3ONp3TK9gX1HYKp6IhetcVybD+2F+Yp6GS9dfH8ue6EOCEzQtQEh4K0FYWBP9qLv+lzeQAYXw+3ySfxyZqkw==" + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.memoize": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", + "dev": true + }, + "lodash.mergewith": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", + "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "2.3.0" + } + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "requires": { + "js-tokens": "3.0.2" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "dev": true, + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3" + }, + "dependencies": { + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + } + } + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0" + } + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=", + "dev": true + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "dev": true, + "requires": { + "mime-db": "1.30.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.9" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "module-deps": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-4.1.1.tgz", + "integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=", + "dev": true, + "requires": { + "JSONStream": "1.3.2", + "browser-resolve": "1.11.2", + "cached-path-relative": "1.0.1", + "concat-stream": "1.5.2", + "defined": "1.0.0", + "detective": "4.7.1", + "duplexer2": "0.1.4", + "inherits": "2.0.3", + "parents": "1.0.1", + "readable-stream": "2.3.3", + "resolve": "1.5.0", + "stream-combiner2": "1.1.1", + "subarg": "1.0.0", + "through2": "2.0.3", + "xtend": "4.0.1" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nan": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", + "dev": true + }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "0.1.12", + "is-stream": "1.1.0" + } + }, + "node-gyp": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.6.2.tgz", + "integrity": "sha1-m/vlRWIoYoSDjnUOrAUpWFP6HGA=", + "dev": true, + "requires": { + "fstream": "1.0.11", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "npmlog": "4.1.2", + "osenv": "0.1.4", + "request": "2.79.0", + "rimraf": "2.6.2", + "semver": "5.3.0", + "tar": "2.2.1", + "which": "1.3.0" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + } + } + }, + "node-sass": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.7.2.tgz", + "integrity": "sha512-CaV+wLqZ7//Jdom5aUFCpGNoECd7BbNhjuwdsX/LkXBrHl8eb1Wjw4HvWqcFvhr5KuNgAk8i/myf/MQ1YYeroA==", + "dev": true, + "requires": { + "async-foreach": "0.1.3", + "chalk": "1.1.3", + "cross-spawn": "3.0.1", + "gaze": "1.1.2", + "get-stdin": "4.0.1", + "glob": "7.1.2", + "in-publish": "2.0.0", + "lodash.assign": "4.2.0", + "lodash.clonedeep": "4.5.0", + "lodash.mergewith": "4.6.1", + "meow": "3.7.0", + "mkdirp": "0.5.1", + "nan": "2.8.0", + "node-gyp": "3.6.2", + "npmlog": "4.1.2", + "request": "2.79.0", + "sass-graph": "2.2.4", + "stdout-stream": "1.4.0", + "true-case-path": "1.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1.1.1" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.5.0", + "validate-npm-package-license": "3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "ora": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-1.4.0.tgz", + "integrity": "sha512-iMK1DOQxzzh2MBlVsU42G80mnrvUhqsMh74phHtDlrcTZPK0pH6o7l7DRshK+0YsxDyEuaOkziVdvM3T0QTzpw==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "cli-cursor": "2.1.0", + "cli-spinners": "1.1.0", + "log-symbols": "2.2.0" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", + "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "outpipe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz", + "integrity": "sha1-UM+GFjZeh+Ax4ppeyTOaPaRyX6I=", + "dev": true, + "requires": { + "shell-quote": "1.6.1" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "dev": true, + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.2.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "dev": true + }, + "parents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", + "dev": true, + "requires": { + "path-platform": "0.11.15" + } + }, + "parse-asn1": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", + "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", + "dev": true, + "requires": { + "asn1.js": "4.9.2", + "browserify-aes": "1.1.1", + "create-hash": "1.1.3", + "evp_bytestokey": "1.0.3", + "pbkdf2": "3.0.14" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "1.0.2" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "1.0.2" + } + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-platform": { + "version": "0.11.15", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pbkdf2": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", + "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==", + "dev": true, + "requires": { + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "ripemd160": "2.0.1", + "safe-buffer": "5.1.1", + "sha.js": "2.4.10" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "postcss": { + "version": "6.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.17.tgz", + "integrity": "sha512-Bl1nybsSzWYbP8O4gAVD8JIjZIul9hLNOPTGBIlVmZNUnNAGL+W0cpYWzVwfImZOwumct4c1SDvSbncVWKtXUw==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "5.1.0" + } + }, + "postcss-cli": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-4.1.1.tgz", + "integrity": "sha1-uUvY//u3rB9i8mB+ePyTl/f2Ol0=", + "dev": true, + "requires": { + "chalk": "2.3.0", + "chokidar": "1.7.0", + "dependency-graph": "0.5.2", + "fs-extra": "4.0.3", + "get-stdin": "5.0.1", + "globby": "6.1.0", + "ora": "1.4.0", + "postcss": "6.0.17", + "postcss-load-config": "1.2.0", + "postcss-reporter": "5.0.0", + "pretty-hrtime": "1.0.3", + "read-cache": "1.0.0", + "yargs": "8.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "get-stdin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", + "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "strip-bom": "3.0.0" + } + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "dev": true, + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "2.3.0" + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "2.0.0", + "normalize-package-data": "2.4.0", + "path-type": "2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "2.1.0", + "read-pkg": "2.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "yargs": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", + "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", + "dev": true, + "requires": { + "camelcase": "4.1.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.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.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "7.0.0" + } + }, + "yargs-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "dev": true, + "requires": { + "camelcase": "4.1.0" + } + } + } + }, + "postcss-load-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", + "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1", + "postcss-load-options": "1.2.0", + "postcss-load-plugins": "2.3.0" + } + }, + "postcss-load-options": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", + "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1" + } + }, + "postcss-load-plugins": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", + "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1" + } + }, + "postcss-reporter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-5.0.0.tgz", + "integrity": "sha512-rBkDbaHAu5uywbCR2XE8a25tats3xSOsGNx6mppK6Q9kSFGKc/FyAzfci+fWM2l+K402p1D0pNcfDGxeje5IKg==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "lodash": "4.17.5", + "log-symbols": "2.2.0", + "postcss": "6.0.17" + } + }, + "postcss-value-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", + "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "2.0.6" + } + }, + "prop-types": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", + "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "public-encrypt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", + "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "parse-asn1": "5.1.0", + "randombytes": "2.0.6" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "qs": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", + "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "randomfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.3.tgz", + "integrity": "sha512-YL6GrhrWoic0Eq8rXVbMptH7dAxCs0J+mh5Y0euNekPPYaxEmdVGim6GdoxoRzKW2yJoU8tueifS7mYxvcFDEQ==", + "dev": true, + "requires": { + "randombytes": "2.0.6", + "safe-buffer": "5.1.1" + } + }, + "react": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.2.0.tgz", + "integrity": "sha512-ZmIomM7EE1DvPEnSFAHZn9Vs9zJl5A9H7el0EGTE6ZbW9FKe/14IYAlPbC8iH25YarEQxZL+E8VW7Mi7kfQrDQ==", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "prop-types": "15.6.0" + } + }, + "react-dom": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.2.0.tgz", + "integrity": "sha512-zpGAdwHVn9K0091d+hr+R0qrjoJ84cIBFL2uU60KvWBPfZ7LPSrfqviTxGHWN0sjPZb2hxWzMexwrvJdKePvjg==", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "prop-types": "15.6.0" + } + }, + "react-redux": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.6.tgz", + "integrity": "sha512-8taaaGu+J7PMJQDJrk/xiWEYQmdo3mkXw6wPr3K3LxvXis3Fymiq7c13S+Tpls/AyNUAsoONkU81AP0RA6y6Vw==", + "requires": { + "hoist-non-react-statics": "2.3.1", + "invariant": "2.2.2", + "lodash": "4.17.5", + "lodash-es": "4.17.5", + "loose-envify": "1.3.1", + "prop-types": "15.6.0" + } + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "requires": { + "pify": "2.3.0" + } + }, + "read-only-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "dev": true, + "requires": { + "readable-stream": "2.3.3" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.3", + "set-immediate-shim": "1.0.1" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "redux": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", + "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", + "requires": { + "lodash": "4.17.5", + "lodash-es": "4.17.5", + "loose-envify": "1.3.1", + "symbol-observable": "1.2.0" + } + }, + "redux-logger": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz", + "integrity": "sha1-91VZZvMJjzyIYExEnPC69XeCdL8=", + "requires": { + "deep-diff": "0.3.8" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "request": { + "version": "2.79.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", + "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "dev": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.11.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "2.0.6", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "qs": "6.3.2", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.4.3", + "uuid": "3.2.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-from-string": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", + "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "resolve": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", + "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "2.0.1", + "signal-exit": "3.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "dev": true, + "requires": { + "hash-base": "2.0.2", + "inherits": "2.0.3" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "dev": true, + "requires": { + "glob": "7.1.2", + "lodash": "4.17.5", + "scss-tokenizer": "0.2.3", + "yargs": "7.1.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "dev": true, + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "y18n": "3.2.1", + "yargs-parser": "5.0.0" + } + } + } + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dev": true, + "requires": { + "js-base64": "2.4.3", + "source-map": "0.4.4" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "sha.js": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.10.tgz", + "integrity": "sha512-vnwmrFDlOExK4Nm16J2KMWHLrp14lBrjxMxBJpu++EnsuBmpiYaM/MEs46Vxxm/4FvdP5yTwuCTO9it5FSjrqA==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + }, + "shasum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", + "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", + "dev": true, + "requires": { + "json-stable-stringify": "0.0.1", + "sha.js": "2.4.10" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true, + "requires": { + "array-filter": "0.0.1", + "array-map": "0.0.0", + "array-reduce": "0.0.0", + "jsonify": "0.0.0" + } + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "socket.io-client": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.0.4.tgz", + "integrity": "sha1-CRilUkBtxeVAs4Dc2Xr8SmQzL44=", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "2.6.9", + "engine.io-client": "3.1.4", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "3.1.2", + "to-array": "0.1.4" + } + }, + "socket.io-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.2.tgz", + "integrity": "sha1-28IoIVH8T6675Aru3Ady66YZ9/I=", + "requires": { + "component-emitter": "1.2.1", + "debug": "2.6.9", + "has-binary2": "1.0.2", + "isarray": "2.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dev": true, + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", + "dev": true + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dev": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "stdout-stream": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz", + "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=", + "dev": true, + "requires": { + "readable-stream": "2.3.3" + } + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, + "stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "dev": true, + "requires": { + "duplexer2": "0.1.4", + "readable-stream": "2.3.3" + } + }, + "stream-http": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.0.tgz", + "integrity": "sha512-sZOFxI/5xw058XIRHl4dU3dZ+TTOIGJR78Dvo0oEAejIt4ou27k+3ne1zYmCV+v7UucbxIFQuOgnkTVHh8YPnw==", + "dev": true, + "requires": { + "builtin-status-codes": "3.0.0", + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "to-arraybuffer": "1.0.1", + "xtend": "4.0.1" + } + }, + "stream-splicer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz", + "integrity": "sha1-G2O+Q4oTPktnHMGTUZdgAXWRDYM=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "dev": true, + "requires": { + "minimist": "1.2.0" + } + }, + "supports-color": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz", + "integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, + "syntax-error": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", + "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", + "dev": true, + "requires": { + "acorn-node": "1.3.0" + } + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true, + "requires": { + "readable-stream": "2.3.3", + "xtend": "4.0.1" + } + }, + "timers-browserify": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "dev": true, + "requires": { + "process": "0.11.10" + } + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "dev": true, + "requires": { + "punycode": "1.4.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "true-case-path": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz", + "integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=", + "dev": true, + "requires": { + "glob": "6.0.4" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "tsconfig": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-5.0.3.tgz", + "integrity": "sha1-X0J45wGACWeo/Dg/0ZZIh48qbjo=", + "dev": true, + "requires": { + "any-promise": "1.3.0", + "parse-json": "2.2.0", + "strip-bom": "2.0.0", + "strip-json-comments": "2.0.1" + } + }, + "tsify": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tsify/-/tsify-3.0.4.tgz", + "integrity": "sha512-y75+qgB41YS8HJck+jmSIn395I4qRGtm5ZELzvNh80Llzh8ojPWp47jm0ZoIJesNYVzbqEyLzgYXV9d/calvVg==", + "dev": true, + "requires": { + "convert-source-map": "1.1.3", + "fs.realpath": "1.0.0", + "object-assign": "4.1.1", + "semver": "5.5.0", + "through2": "2.0.3", + "tsconfig": "5.0.3" + } + }, + "tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typescript": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.1.tgz", + "integrity": "sha512-bqB1yS6o9TNA9ZC/MJxM0FZzPnZdtHj0xWK/IZ5khzVqdpGul/R/EIiHRgFXlwTD7PSIaYVnGKq1QgMCu2mnqw==", + "dev": true + }, + "ua-parser-js": { + "version": "0.7.17", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz", + "integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==" + }, + "uglify-es": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.10.tgz", + "integrity": "sha512-rPzPisCzW68Okj1zNrfa2dR9uEm43SevDmpR6FChoZABFk9dANGnzzBMgHYUXI3609//63fnVkyQ1SQmAMyjww==", + "dev": true, + "requires": { + "commander": "2.14.1", + "source-map": "0.6.1" + } + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" + }, + "umd": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.1.tgz", + "integrity": "sha1-iuVW4RAR9jwllnCKiDclnwGz1g4=", + "dev": true + }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dev": true, + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true, + "requires": { + "indexof": "0.0.1" + } + }, + "watchify": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/watchify/-/watchify-3.10.0.tgz", + "integrity": "sha512-SRSumWalHAxciSaEtua1HFqB8L+et5ieHjJRuNssqj4qXz4pJoR6cAeFWni3reXyOY3cVE6b55sJ8WYR43abBQ==", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "browserify": "15.2.0", + "chokidar": "1.7.0", + "defined": "1.0.0", + "outpipe": "1.1.1", + "through2": "2.0.3", + "xtend": "4.0.1" + }, + "dependencies": { + "acorn": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.4.1.tgz", + "integrity": "sha512-XLmq3H/BVvW6/GbxKryGxWORz1ebilSsUDlyC27bXhWGWAZWkGwS6FLHjOlwFXNFoWFQEO/Df4u0YYd0K3BQgQ==", + "dev": true + }, + "browserify": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-15.2.0.tgz", + "integrity": "sha512-IHYyFPm2XjJCL+VV0ZtFv8wn/sAHVOm83q3yfSn8YWbZ9jcybgPKxSDdiuMU+35jUL1914l74RnXXPD9Iyo9yg==", + "dev": true, + "requires": { + "JSONStream": "1.3.2", + "assert": "1.4.1", + "browser-pack": "6.0.4", + "browser-resolve": "1.11.2", + "browserify-zlib": "0.2.0", + "buffer": "5.0.8", + "cached-path-relative": "1.0.1", + "concat-stream": "1.5.2", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.12.0", + "defined": "1.0.0", + "deps-sort": "2.0.0", + "domain-browser": "1.1.7", + "duplexer2": "0.1.4", + "events": "1.1.1", + "glob": "7.1.2", + "has": "1.0.1", + "htmlescape": "1.1.1", + "https-browserify": "1.0.0", + "inherits": "2.0.3", + "insert-module-globals": "7.0.1", + "labeled-stream-splicer": "2.0.0", + "mkdirp": "0.5.1", + "module-deps": "5.0.1", + "os-browserify": "0.3.0", + "parents": "1.0.1", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "read-only-stream": "2.0.0", + "readable-stream": "2.3.3", + "resolve": "1.5.0", + "shasum": "1.0.2", + "shell-quote": "1.6.1", + "stream-browserify": "2.0.1", + "stream-http": "2.8.0", + "string_decoder": "1.0.3", + "subarg": "1.0.0", + "syntax-error": "1.4.0", + "through2": "2.0.3", + "timers-browserify": "1.4.2", + "tty-browserify": "0.0.1", + "url": "0.11.0", + "util": "0.10.3", + "vm-browserify": "0.0.4", + "xtend": "4.0.1" + } + }, + "detective": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.0.2.tgz", + "integrity": "sha512-NUsLoezj4wb9o7vpxS9F3L5vcO87ceyRBcl48op06YFNwkyIEY997JpSCA5lDlDuDc6JxOtaL5qfK3muoWxpMA==", + "dev": true, + "requires": { + "@browserify/acorn5-object-spread": "5.0.1", + "acorn": "5.4.1", + "defined": "1.0.0" + } + }, + "module-deps": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-5.0.1.tgz", + "integrity": "sha512-sigq/hm/L+Z5IGi1DDl0x2ptkw7S86aFh213QhPLD8v9Opv90IHzKIuWJrRa5bJ77DVKHco2CfIEuThcT/vDJA==", + "dev": true, + "requires": { + "JSONStream": "1.3.2", + "browser-resolve": "1.11.2", + "cached-path-relative": "1.0.1", + "concat-stream": "1.6.0", + "defined": "1.0.0", + "detective": "5.0.2", + "duplexer2": "0.1.4", + "inherits": "2.0.3", + "parents": "1.0.1", + "readable-stream": "2.3.3", + "resolve": "1.5.0", + "stream-combiner2": "1.1.1", + "subarg": "1.0.0", + "through2": "2.0.3", + "xtend": "4.0.1" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "typedarray": "0.0.6" + } + } + } + } + } + }, + "whatwg-fetch": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", + "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "dev": true, + "requires": { + "string-width": "1.0.2" + } + }, + "window-size": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=", + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.1", + "ultron": "1.1.1" + } + }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "os-locale": "1.4.0", + "string-width": "1.0.2", + "window-size": "0.1.4", + "y18n": "3.2.1" + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "dev": true, + "requires": { + "camelcase": "3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + } + } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + } + } +} diff --git a/gui/package.json b/gui/package.json new file mode 100644 index 0000000..0c287a8 --- /dev/null +++ b/gui/package.json @@ -0,0 +1,48 @@ +{ + "name": "sshmon-gui", + "version": "", + "scripts": { + "mkbdir": "mkdir -p public/build", + "css-compile": "node-sass --recursive -o public/build/css sass", + "css-postcss": "postcss --use autoprefixer -d public/build/postcss --no-map public/build/css/*", + "css-build": "npm run css-compile && npm run css-postcss", + "css-watch": "npm run css-compile && (npm run css-compile -- --watch & npm run css-postcss -- --watch)", + "js-compile": "browserify src/index.tsx -o public/build/bundle.js --debug -p tsify", + "js-compile-watch": "watchify src/index.tsx -o public/build/bundle.js --debug -p tsify", + "js-uglify": "uglifyjs --source-map content=inline,url=bundle.min.js.map -o public/build/bundle.min.js -- public/build/bundle.js", + "js-uglify-watch": "chokidar public/build/bundle.js -c 'npm run js-uglify'", + "js-build": "npm run js-compile && npm run js-uglify", + "js-watch": "npm run js-compile-watch & npm run js-uglify-watch", + "build": "npm run mkbdir && npm run css-build && npm run js-build", + "build-watch": "npm run mkbdir && npm run css-watch & npm run js-watch", + "clean": "! test -e public/build || rm -r public/build", + "lint": "../node_modules/.bin/tslint -p ." + }, + "dependencies": { + "bulma": "^0.6.2", + "react": "^16.2.0", + "react-dom": "^16.2.0", + "react-redux": "^5.0.6", + "redux": "^3.7.2", + "redux-logger": "^3.0.6", + "socket.io-client": "^2.0.4" + }, + "devDependencies": { + "@types/react": "^16.0.36", + "@types/react-dom": "^16.0.3", + "@types/react-redux": "^5.0.14", + "@types/redux-logger": "^3.0.5", + "@types/socket.io-client": "^1.4.32", + "autoprefixer": "^7.2.3", + "browserify": "^14.5.0", + "bulmaswatch": "^0.6.2", + "chokidar-cli": "^1.2.0", + "font-awesome": "^4.7.0", + "node-sass": "^4.7.2", + "postcss-cli": "^4.1.1", + "tsify": "^3.0.4", + "typescript": "^2.7.1", + "uglify-es": "^3.3.5", + "watchify": "^3.9.0" + } +} diff --git a/gui/public/index.html b/gui/public/index.html new file mode 100644 index 0000000..a927886 --- /dev/null +++ b/gui/public/index.html @@ -0,0 +1,12 @@ + + + + + + sshmon + + + +
+ + diff --git a/gui/sass/main.scss b/gui/sass/main.scss new file mode 100644 index 0000000..fa973ef --- /dev/null +++ b/gui/sass/main.scss @@ -0,0 +1,50 @@ +@charset "utf-8"; + +@import "../node_modules/font-awesome/scss/font-awesome"; + +@import "../node_modules/bulmaswatch/darkly/_variables"; +@import "../node_modules/bulma/bulma"; +@import "../node_modules/bulmaswatch/darkly/_overrides"; + +.panel { + >:first-child { + border-top-left-radius: $panel-heading-radius; + border-top-right-radius: $panel-heading-radius; + } + >:last-child { + border-bottom-left-radius: $panel-heading-radius; + border-bottom-right-radius: $panel-heading-radius; + } +} + +// square icon buttons +.button { + height: auto; + width: auto; +} + +// darkly +.panel-block { + &:hover { + color: inherit; + } +} +.table { + background-color: inherit; +} + +// add border around tags to increase visibility (e.g. for darkly) +.panel-block:first-child { + .tag { + box-shadow: 0 0 0 0.1em rgba(#eeeeee, 0.1) inset; + } +} + +// increase disabled button visibility (e.g. for darkly) +.button[disabled] { + background-color: transparent; + border-color: transparent; + box-shadow: none; +} + +// recommended themes: default, litera, cyborg, flatly, superhero diff --git a/gui/src/api/actions.ts b/gui/src/api/actions.ts new file mode 100644 index 0000000..adca89d --- /dev/null +++ b/gui/src/api/actions.ts @@ -0,0 +1,18 @@ +import { State as APIState } from '../../../server/src/types/redux' + +export enum types { + API_STATE_CHANGE = 'API_STATE_CHANGE', + API_STATUS_CHANGE = 'API_STATUS_CHANGE' +} + +export type APIStatus = 'connected' | 'disconnected' + +export type Action = + | { type: types.API_STATE_CHANGE, state: APIState } + | { type: types.API_STATUS_CHANGE, status: APIStatus } + +export const actions = { + apiStateChange: (state: APIState): Action => ({ type: types.API_STATE_CHANGE, state }), + apiStatusConnected: (): Action => ({ type: types.API_STATUS_CHANGE, status: 'connected' }), + apiStatusDisconnected: (): Action => ({ type: types.API_STATUS_CHANGE, status: 'disconnected' }) +} diff --git a/gui/src/api/client.ts b/gui/src/api/client.ts new file mode 100644 index 0000000..4d566ac --- /dev/null +++ b/gui/src/api/client.ts @@ -0,0 +1,63 @@ +import * as io from 'socket.io-client' + +import { actions } from './actions' +import { APIEndpoint, apiKeys } from '../../../server/src/api/api' +import { SOCKET_PATH, socketTypes, SocketMessageError } from '../../../server/src/api/constants' +import { AutoconnectConfig } from '../../../server/src/autoconnect/types' +import { AutoforwardConfig } from '../../../server/src/autoforward/types' +import { HostConfig } from '../../../server/src/host/types' +import { ForwardingConfig } from '../../../server/src/forward/types' +import { State as APIState } from '../../../server/src/types/redux' +import { Store } from '../types/redux' + +const setup = (socket: SocketIOClient.Socket, store: Store) => { + socket.on('connect', () => { + console.log('connected to socket server') + store.dispatch(actions.apiStatusConnected()) + socket.emit(socketTypes.register) + }) + + socket.on('disconnect', () => { + console.log('disconnected from socket server') + store.dispatch(actions.apiStatusDisconnected()) + }) + + socket.on(socketTypes.state, (state: APIState) => { + store.dispatch(actions.apiStateChange(state)) + + // FIXME hpello + if (state.system.info) { + document.title = `${state.system.info.hostName} | sshmon` + } + }) +} + +export class APIClient { + store: Store + socket: SocketIOClient.Socket + + constructor(store: Store) { + this.store = store + this.socket = io({ path: `${window.location.pathname}${SOCKET_PATH}`.replace(/\/{2,}/g, '/') }) + setup(this.socket, store) + } + + makeAPICall(e: APIEndpoint) { + this.socket.emit(socketTypes.apiCall, e, (err: SocketMessageError | null, result: any) => { + if (err) { console.error(`API call failure: ${err.message}`, e); return } + console.log('API call success:', e, { result }) + }) + } + + hostCreate(args: { id: string, config: HostConfig, autoConfig: AutoconnectConfig }) { this.makeAPICall({ key: apiKeys.hostCreate, args }) } + hostEdit(args: { id: string, config: HostConfig, autoConfig: AutoconnectConfig }) { this.makeAPICall({ key: apiKeys.hostEdit, args }) } + hostDelete(args: { id: string }) { this.makeAPICall({ key: apiKeys.hostDelete, args }) } + hostConnect(args: { id: string }) { this.makeAPICall({ key: apiKeys.hostConnect, args }) } + hostDisconnect(args: { id: string }) { this.makeAPICall({ key: apiKeys.hostDisconnect, args }) } + + forwardingCreate(args: { id: string, fwdId: string, config: ForwardingConfig, autoConfig: AutoforwardConfig }) { this.makeAPICall({ key: apiKeys.forwardingCreate, args }) } + forwardingEdit(args: { id: string, fwdId: string, config: ForwardingConfig, autoConfig: AutoforwardConfig }) { this.makeAPICall({ key: apiKeys.forwardingEdit, args }) } + forwardingDelete(args: { id: string, fwdId: string }) { this.makeAPICall({ key: apiKeys.forwardingDelete, args }) } + forwardingConnect(args: { id: string, fwdId: string }) { this.makeAPICall({ key: apiKeys.forwardingConnect, args }) } + forwardingDisconnect(args: { id: string, fwdId: string }) { this.makeAPICall({ key: apiKeys.forwardingDisconnect, args }) } +} diff --git a/gui/src/api/reducer.ts b/gui/src/api/reducer.ts new file mode 100644 index 0000000..5e05378 --- /dev/null +++ b/gui/src/api/reducer.ts @@ -0,0 +1,33 @@ +import { APIStatus, types } from './actions' +import { State as APIState } from '../../../server/src/types/redux' +import { Action } from '../types/redux' + +export type State = { + state: APIState, + status: APIStatus +} + +const initialState = (): State => ({ + state: { + hosts: [], + forwardings: [], + autoconnects: [], + autoforwards: [], + system: { + info: null, + stats: null + }, + config: { + autosave: false + } + }, + status: 'disconnected' +}) + +export const reducer = (state: State = initialState(), action: Action): State => { + switch (action.type) { + case types.API_STATE_CHANGE: return { ...state, state: action.state } + case types.API_STATUS_CHANGE: return { ...state, status: action.status } + default: return state + } +} diff --git a/gui/src/components/App.tsx b/gui/src/components/App.tsx new file mode 100644 index 0000000..ea77e3e --- /dev/null +++ b/gui/src/components/App.tsx @@ -0,0 +1,87 @@ +import * as React from 'react' +import { connect } from 'react-redux' + +import { APIClient } from '../api/client' +import { State as APIState } from '../api/reducer' +import { State } from '../types/redux' + +import Header from './Header' +import Host from './Host' +import HostCreate from './HostCreate' +import SystemInfo from './SystemInfo' + +interface OwnProps { + apiClient: APIClient +} + +interface StateProps { + apiState: APIState +} + +interface Props extends StateProps, OwnProps { } + +const APIDisconnectedModal = (props: { active: boolean }) => ( +
+
+
+
+
+

API unreachable

+
+
+

sshmon API appears to be down

+

Please wait for reconnection...

+
+
+
+
+) + +class App extends React.Component { + render() { + const { info } = this.props.apiState.state.system + return ( +
+
+ {info.user} @ {info.hostName} + } /> + +
+
+
+
+

Hosts

+ + {this.props.apiState.state.hosts.map(({ id }) => { + return ( + + ) + })} + + +
+ +
+

Info

+ +
+
+
+
+ + +
+ ) + } +} + +const mapStateToProps = (state: State, ownProps: OwnProps): Props => ({ + apiClient: ownProps.apiClient, + apiState: state.api +}) + +export default connect(mapStateToProps)(App) diff --git a/gui/src/components/Forwarding.tsx b/gui/src/components/Forwarding.tsx new file mode 100644 index 0000000..c0f8a24 --- /dev/null +++ b/gui/src/components/Forwarding.tsx @@ -0,0 +1,260 @@ +import * as React from 'react' +import { connect } from 'react-redux' + +import { State } from '../types/redux' + +import { PROXY_PATH_PREFIX } from '../../../server/src/api/constants' +import { AutoforwardState } from '../../../server/src/autoforward/reducer' +import { ForwardingStatus } from '../../../server/src/forward/actions' +import { ForwardingState, ForwardingSubState } from '../../../server/src/forward/reducer' +import { ForwardingConfig, ForwardingSpec, fwdTypes } from '../../../server/src/forward/types' + +import { APIClient } from '../api/client' + +import ForwardingForm from './ForwardingForm' + +interface OwnProps { + id: string, + fwdId: string, + apiClient: APIClient +} + +interface StateProps { + state: ForwardingState | null, + autoforward: AutoforwardState | null, + hostIsUp: boolean, +} + +interface Props extends StateProps, OwnProps { } + +interface ComponentState { + editIsActive: boolean +} + +const makeColorClass = (status: ForwardingStatus): string => { + switch (status) { + case 'connecting': return 'has-text-info' + case 'connected': return 'has-text-success' + case 'disconnecting': return 'has-text-warning' + case 'disconnected': return 'has-text-grey' + case 'error': return 'has-text-danger' + default: return '' + } +} + +const makeStatusText = (status: ForwardingStatus) => { + switch (status) { + case 'connecting': return 'activating' + case 'connected': return 'active' + case 'disconnecting': return 'canceling' + case 'disconnected': return 'inactive' + case 'error': return 'error' + default: return '' + } +} + +const makeStatus = (state: ForwardingSubState) => { + const colorClass = makeColorClass(state.status) + const text = makeStatusText(state.status) + + return ( +
+
+ forwarding{/* force width of maximum contents */} + {text} +
+
+ + + + + +
+
+ ) +} + +const makeForwardingHref = (spec: ForwardingSpec): string => { + switch (spec.type) { + case fwdTypes.local: + case fwdTypes.dynamic: + if (spec.bind.includes('/')) { return `unix://${spec.bind}` } + if (`${parseInt(spec.bind, 10)}` === spec.bind) { return `//localhost:${spec.bind}` } + return `//${spec.bind}` + default: return '' + } +} + +const makeCodeElement = (child: React.ReactNode): React.ReactNode => { + return ( + {/* min width to match 5 digit ports */} + {child} + + ) +} + +const makeForwardingDescription = (config: ForwardingConfig, isConnected: boolean, id: string, fwdId: string) => { + const { spec } = config + switch (spec.type) { + case fwdTypes.dynamic: return ( + + {makeCodeElement( + isConnected + ? {spec.bind} + : spec.bind + )} + + ) + case fwdTypes.local: return ( + + {makeCodeElement( + isConnected + ? {spec.bind} + : spec.bind + )} + : + {makeCodeElement(spec.target)} + + ) + case fwdTypes.remote: return ( + + {makeCodeElement(spec.bind)} + : + {makeCodeElement(spec.target)} + + ) + case fwdTypes.http: return ( + + {makeCodeElement( + isConnected + ? + + + : + )} + : + {makeCodeElement(spec.target)} + + ) + } +} + +class Forwarding extends React.Component { + state = { editIsActive: false } + + render() { + if (!this.props.state || !this.props.autoforward) { return null } + const { id, fwdId, config, state } = this.props.state + const { apiClient } = this.props + + return (<> +
+ +
+
+ +
+ {['disconnected', 'error'].includes(state.status) ? ( + + ) : ( + + )} +
+
+ +
+ +
+
+ +
+
+ +
+
+
+ + {config.label || fwdId} + +
+
+
+ {this.props.autoforward.config.start ?
start
: null} + {this.props.autoforward.config.retry ?
retry
: null} +
+
+
+
+ +
+
+
+ + dynamic{/* force width of maximum contents */} + + {config.spec.type} + + +
+
+ {config.spec.type.charAt(0)} +
+
+ {makeForwardingDescription(config, state.status === 'connected', id, fwdId)} +
+
+
+ +
+
+ +
+ {makeStatus(state)} +
+ +
+ + {!this.state.editIsActive ? null : ( + this.setState({ ...this.state, editIsActive: false })} + /> + )} + ) + } +} + +const makeHostIsUp = (state: State, id: string): boolean => { + const host = state.api.state.hosts.find(x => x.id === id) + return host ? host.state.status === 'connected' : false +} + +const mapStateToProps = (state: State, ownProps: OwnProps): Props => { + const hostIsUp = makeHostIsUp(state, ownProps.id) + const forwarding = state.api.state.forwardings.find(x => x.id === ownProps.id && x.fwdId === ownProps.fwdId) || null + const autoforward = state.api.state.autoforwards.find(x => x.id === ownProps.id && x.fwdId === ownProps.fwdId) || null + return { + ...ownProps, + state: forwarding, + autoforward, + hostIsUp + } +} + +export default connect(mapStateToProps)(Forwarding) diff --git a/gui/src/components/ForwardingCreate.tsx b/gui/src/components/ForwardingCreate.tsx new file mode 100644 index 0000000..47a04e1 --- /dev/null +++ b/gui/src/components/ForwardingCreate.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' + +import { APIClient } from '../api/client' + +import ForwardingForm from './ForwardingForm' + +interface Props { + id: string, + apiClient: APIClient +} + +interface ComponentState { + editIsActive: boolean +} + +export default class ForwardingCreate extends React.Component { + state = { editIsActive: false } + + render() { + return (<> +
+
+
this.setState({ ...this.state, editIsActive: true })}> + + + +
+
+
+ New forwarding… +
+
+ + {!this.state.editIsActive ? null : ( + this.setState({ ...this.state, editIsActive: false })} + /> + )} + ) + } +} diff --git a/gui/src/components/ForwardingForm.tsx b/gui/src/components/ForwardingForm.tsx new file mode 100644 index 0000000..204d14c --- /dev/null +++ b/gui/src/components/ForwardingForm.tsx @@ -0,0 +1,332 @@ +import * as React from 'react' +import { connect } from 'react-redux' + +import { APIClient } from '../api/client' +import { AutoforwardConfig } from '../../../server/src/autoforward/types' +import { ForwardingState } from '../../../server/src/forward/reducer' +import { ForwardingConfig, ForwardingSpec, fwdTypes } from '../../../server/src/forward/types' + +import { State } from '../types/redux' + +interface OwnProps { + id: string, + fwdId: string | null, + config: ForwardingConfig | null, + autoConfig: AutoforwardConfig | null, + apiClient: APIClient + onClose: () => void +} + +interface StateProps { + forwardings: ForwardingState[] +} + +interface Props extends StateProps, OwnProps { } + +interface ComponentState { + readonly id: string, + fwdId: string, + label: string, + autostart: boolean, + autoretry: boolean, + type: fwdTypes, + bind: string, + target: string +} + +const makeSpec = (type: fwdTypes, bind: string, target: string): ForwardingSpec => { + switch (type) { + case fwdTypes.dynamic: + return { type, bind } + case fwdTypes.local: + case fwdTypes.remote: + return { type, bind, target } + case fwdTypes.http: + return { type, target } + } +} + +const extractBind = (spec: ForwardingSpec): string => { + switch (spec.type) { + case fwdTypes.dynamic: + case fwdTypes.local: + case fwdTypes.remote: + return spec.bind + case fwdTypes.http: + return '' + } +} + +const extractTarget = (spec: ForwardingSpec): string => { + switch (spec.type) { + case fwdTypes.local: + case fwdTypes.remote: + case fwdTypes.http: + return spec.target + case fwdTypes.dynamic: + return '' + } +} + +class ForwardingForm extends React.Component { + constructor(props: Props) { + super(props) + this.state = { + id: props.id, + fwdId: props.fwdId || '', + label: props.config ? props.config.label : '', + autostart: props.autoConfig ? props.autoConfig.start : false, + autoretry: props.autoConfig ? props.autoConfig.retry : false, + type: props.config ? props.config.spec.type : fwdTypes.local, + bind: props.config ? extractBind(props.config.spec) : '', + target: props.config ? extractTarget(props.config.spec) : '' + } + } + + isCreate(): boolean { + return this.props.fwdId === null + } + + checkIsValidFwdId(): string { + if (!this.isCreate()) { return '' } + if (this.state.fwdId === '') { return 'Forwarding ID is required.' } + if (this.props.forwardings + .filter(x => x.id === this.props.id) + .map(x => x.fwdId) + .includes(this.state.fwdId)) { return 'Forwarding ID is already taken.' } + + return '' + } + + getErrors(): string[] { + return [ + this.checkIsValidFwdId() + ].filter(x => x) + } + + submit() { + const { id, fwdId, label, type, bind, target, autostart, autoretry } = this.state + const spec = makeSpec(type, bind, target) + const config = { + label, + spec + } + const autoConfig = { start: autostart, retry: autoretry } + if (this.isCreate()) { + this.props.apiClient.forwardingCreate({ id, fwdId, config, autoConfig }) + } else { + this.props.apiClient.forwardingEdit({ id, fwdId, config, autoConfig }) + } + + this.props.onClose() + } + + delete() { + if (this.props.fwdId === null) { return } + this.props.apiClient.forwardingDelete({ id: this.props.id, fwdId: this.props.fwdId }) + + this.props.onClose() + } + + isValid(): boolean { + return this.getErrors().length === 0 + } + + onChangeFwdId(event: React.ChangeEvent<{ value: string }>) { this.setState({ ...this.state, fwdId: event.target.value }) } + onChangeLabel(event: React.ChangeEvent<{ value: string }>) { this.setState({ ...this.state, label: event.target.value }) } + onChangeAutostart(event: React.ChangeEvent<{ value: 'on' | 'off' }>) { this.setState({ ...this.state, autostart: event.target.value === 'on' }) } + onChangeAutoretry(event: React.ChangeEvent<{ value: 'on' | 'off' }>) { this.setState({ ...this.state, autoretry: event.target.value === 'on' }) } + onChangeType(event: React.ChangeEvent<{ value: fwdTypes }>) { this.setState({ ...this.state, type: event.target.value, bind: '', target: '' }) } + onChangeBind(event: React.ChangeEvent<{ value: string }>) { this.setState({ ...this.state, bind: event.target.value }) } + onChangeTarget(event: React.ChangeEvent<{ value: string }>) { this.setState({ ...this.state, target: event.target.value }) } + + render() { + return ( +
+
+
+
+

{this.isCreate() ? 'New' : 'Edit'} forwarding

+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
{this.checkIsValidFwdId()}
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
{/* TODO hpello add some help here? */}
+
+
+
+ +
+
+ +
+
+
+
+ +
+
{/* TODO hpello add some help here? */}
+
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
+
+ + {this.isCreate() ? null : (<> +
+ +
Delete forwarding
+ +
+
+ +
+
This cannot be undone.
+
+ )} +
+
{/* INFO hpello .buttons modifier is-right does not seem to work without block style */} +
+ + +
+
+
+
+ ) + } +} + +const mapStateToProps = (state: State, ownProps: OwnProps): Props => ({ + ...ownProps, + forwardings: state.api.state.forwardings +}) + +export default connect(mapStateToProps)(ForwardingForm) diff --git a/gui/src/components/Header.tsx b/gui/src/components/Header.tsx new file mode 100644 index 0000000..e9eecb3 --- /dev/null +++ b/gui/src/components/Header.tsx @@ -0,0 +1,21 @@ +import * as React from 'react' + +type Props = { + subtitle: React.ReactNode | null +} + +const Header = (props: Props) => ( +
+
+
+

+ sshmon +

+

+ {props.subtitle} +

+
+
+
+) +export default Header diff --git a/gui/src/components/Host.tsx b/gui/src/components/Host.tsx new file mode 100644 index 0000000..271850b --- /dev/null +++ b/gui/src/components/Host.tsx @@ -0,0 +1,224 @@ +import * as React from 'react' +import { connect } from 'react-redux' + +import { State } from '../types/redux' + +import { ForwardingState } from '../../../server/src/forward/reducer' +import { HostStatus } from '../../../server/src/host/actions' +import { HostState, HostSubState } from '../../../server/src/host/reducer' +import { CONNECT_REASON_AUTORETRY } from '../../../server/src/autoconnect/constants' +import { AutoconnectState, AutoconnectSubState } from '../../../server/src/autoconnect/reducer' + +import { APIClient } from '../api/client' + +import HostForm from './HostForm' +import Forwarding from './Forwarding' +import ForwardingCreate from './ForwardingCreate' + +interface OwnProps { + id: string, + apiClient: APIClient +} + +interface StateProps { + state: HostState | null, + forwardings: ForwardingState[], + autoconnect: AutoconnectState | null +} + +interface ComponentState { + isCollapsed: boolean + contentHeight: number + editIsActive: boolean +} + +interface Props extends StateProps, OwnProps { } + +const makeColorClass = (status: HostStatus): string => { + switch (status) { + case 'connecting': return 'has-text-info' + case 'connected': return 'has-text-success' + case 'disconnecting': return 'has-text-warning' + case 'disconnected': return 'has-text-grey' + case 'error': return 'has-text-danger' + default: return '' + } +} + +const makeStatusText = (state: HostSubState, autoconnect: AutoconnectSubState) => { + if (autoconnect.autoretryId && state.status !== 'connecting') { + if (autoconnect.timeout === 0) { return 'Reconnecting' } + return `Reconnecting in ${autoconnect.timeout / 1000}s…` + } + if (state.status === 'connecting' && state.reason === CONNECT_REASON_AUTORETRY) { + return 'Reconnecting' + } + + return {state.status} +} + +const makeStatus = (state: HostSubState, autoconnect: AutoconnectSubState) => { + const colorClass = (state.status !== 'connecting' && autoconnect.autoretryId) ? 'has-text-info' : makeColorClass(state.status) + const text = makeStatusText(state, autoconnect) + + return ( +
+
+ {text} +
+
+ + + + + +
+
+ ) +} + +const makePanelBlockStyle = (state: ComponentState) => { + const style: React.CSSProperties = { + overflow: 'hidden', + transition: 'all 200ms ease-in-out' + } + + if (state.isCollapsed) { + style.maxHeight = 0 + style.borderBottomWidth = 0 + style.paddingBottom = 0 + style.paddingTop = 0 + style.marginBottom = 0 + style.marginTop = 0 + } else { + style.maxHeight = state.contentHeight * 1.5 // add some more to leave space for padding/margin + } + + return style +} + +class Host extends React.Component { + state = { isCollapsed: true, contentHeight: 0, editIsActive: false } + panelBlock: HTMLDivElement | null = null + + computeContentHeight() { + if (this.panelBlock === null) { return } + if (this.state.isCollapsed) { return } + + const contentHeight = this.panelBlock.scrollHeight + + if (contentHeight !== this.state.contentHeight) { + this.setState({ ...this.state, contentHeight }) + } + } + + componentDidMount() { this.computeContentHeight() } + componentDidUpdate() { this.computeContentHeight() } + + onClick() { + this.setState({ ...this.state, isCollapsed: !this.state.isCollapsed }) + } + + render() { + if (!this.props.state || !this.props.autoconnect) { return null } + const { id, config, state } = this.props.state + const { apiClient } = this.props + + return (<> +
+
+
+
+ + + +
+
+
+
+
{config.label || id}
+
+
+
+ {this.props.autoconnect.config.start ?
start
: null} + {this.props.autoconnect.config.retry ?
retry
: null} +
+
+
+
+
+ {makeStatus(state, this.props.autoconnect.state)} +
+
+
+
{ this.panelBlock = ref }} + > + + + {this.props.forwardings.map(({ fwdId }) => ( + + + + ))} + + + + +
+ +
+ +
+
+
{/* https://github.com/jgthms/bulma/issues/1563 */} +
+
+
+
+ + {['disconnected', 'error'].includes(state.status) ? ( + + ) : ( + + )} +
+
+
+
+
+ + {!this.state.editIsActive ? null : ( + this.setState({ ...this.state, editIsActive: false })} + /> + )} + ) + } +} + +const mapStateToProps = (state: State, ownProps: OwnProps): Props => ({ + ...ownProps, + state: state.api.state.hosts.find(x => x.id === ownProps.id) || null, + forwardings: state.api.state.forwardings.filter(x => x.id === ownProps.id), + autoconnect: state.api.state.autoconnects.find(x => x.id === ownProps.id) || null +}) + +export default connect(mapStateToProps)(Host) diff --git a/gui/src/components/HostCreate.tsx b/gui/src/components/HostCreate.tsx new file mode 100644 index 0000000..551d14b --- /dev/null +++ b/gui/src/components/HostCreate.tsx @@ -0,0 +1,35 @@ +import * as React from 'react' + +import { APIClient } from '../api/client' + +import HostForm from './HostForm' + +interface Props { + apiClient: APIClient +} + +interface ComponentState { + editIsActive: boolean +} + +export default class HostCreate extends React.Component { + state = { editIsActive: false } + + render() { + return (<> +
+
this.setState({ ...this.state, editIsActive: true })}>New host…
+
+ + {!this.state.editIsActive ? null : ( + this.setState({ ...this.state, editIsActive: false })} + /> + )} + ) + } +} diff --git a/gui/src/components/HostForm.tsx b/gui/src/components/HostForm.tsx new file mode 100644 index 0000000..ff5f8f3 --- /dev/null +++ b/gui/src/components/HostForm.tsx @@ -0,0 +1,284 @@ +import * as React from 'react' +import { connect } from 'react-redux' + +import { APIClient } from '../api/client' +import { AutoconnectConfig } from '../../../server/src/autoconnect/types' +import { HostState } from '../../../server/src/host/reducer' +import { HostConfig } from '../../../server/src/host/types' + +import { State } from '../types/redux' + +interface OwnProps { + id: string | null, + config: HostConfig | null, + autoConfig: AutoconnectConfig | null, + apiClient: APIClient + onClose: () => void +} + +interface StateProps { + hosts: HostState[] +} + +interface Props extends StateProps, OwnProps { } + +interface ComponentState { + id: string, + label: string, + autostart: boolean, + autoretry: boolean, + sshHost: string, + sshConfig: { key: string, value: string }[] +} + +class HostForm extends React.Component { + constructor(props: Props) { + super(props) + this.state = { + id: props.id || '', + label: props.config ? props.config.label : '', + autostart: props.autoConfig ? props.autoConfig.start : false, + autoretry: props.autoConfig ? props.autoConfig.retry : false, + sshHost: props.config && (props.config.ssh.host !== props.id) ? props.config.ssh.host : '', + sshConfig: props.config + ? Object.entries(props.config.ssh.config).map(x => ({ key: x[0], value: x[1] })) + : [] + } + } + + isCreate(): boolean { + return this.props.id === null + } + + checkIsValidId(): string { + if (!this.isCreate()) { return '' } + if (this.state.id === '') { return 'ID is required.' } + if (this.props.hosts.map(x => x.id).includes(this.state.id)) { return 'ID is already taken.' } + return '' + } + + getErrors(): string[] { + return [ + this.checkIsValidId() + ].filter(x => x) + } + + submit() { + const { id, label, sshHost, sshConfig, autostart, autoretry } = this.state + const config = { + label, + ssh: { + host: sshHost || id, + config: sshConfig.reduce((acc, val) => { + acc[val.key] = val.value + return acc + }, {} as { [key: string]: string }) + } + } + const autoConfig = { start: autostart, retry: autoretry } + if (this.isCreate()) { + this.props.apiClient.hostCreate({ id, config, autoConfig }) + } else { + this.props.apiClient.hostEdit({ id, config, autoConfig }) + } + + this.props.onClose() + } + + delete() { + if (this.props.id === null) { return } + this.props.apiClient.hostDelete({ id: this.props.id }) + + this.props.onClose() + } + + isValid(): boolean { + return this.getErrors().length === 0 + } + + onChangeId(event: React.ChangeEvent<{ value: string }>) { this.setState({ ...this.state, id: event.target.value }) } + onChangeLabel(event: React.ChangeEvent<{ value: string }>) { this.setState({ ...this.state, label: event.target.value }) } + onChangeAutostart(event: React.ChangeEvent<{ value: 'on' | 'off' }>) { this.setState({ ...this.state, autostart: event.target.value === 'on' }) } + onChangeAutoretry(event: React.ChangeEvent<{ value: 'on' | 'off' }>) { this.setState({ ...this.state, autoretry: event.target.value === 'on' }) } + onChangeSSHHost(event: React.ChangeEvent<{ value: string }>) { this.setState({ ...this.state, sshHost: event.target.value }) } + onChangeSSHConfigKey(index: number, event: React.ChangeEvent<{ value: string }>) { + const sshConfig = [...this.state.sshConfig, { key: '', value: '' }].map((x, i) => i === index + ? { key: event.target.value, value: x.value } + : x + ) + .filter(x => x.key || x.value) + this.setState({ ...this.state, sshConfig }) + } + onChangeSSHConfigValue(index: number, event: React.ChangeEvent<{ value: string }>) { + const sshConfig = [...this.state.sshConfig, { key: '', value: '' }].map((x, i) => i === index + ? { key: x.key, value: event.target.value } + : x + ) + .filter(x => x.key || x.value) + this.setState({ ...this.state, sshConfig }) + } + + render() { + return ( +
+
+
+
+

{this.isCreate() ? 'New' : 'Edit'} host

+ +
+
+ +
+
+ +
+
+
+
+ +
+
{this.checkIsValidId()}
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
+
+ +
+ +
SSH options
+ +
+
+ +
+
+
+
+ +
+
Defaults to ID if not specified.
+
+
+
+ {[...this.state.sshConfig, { key: '', value: '' }].map((x, i) => ( +
+
+ +
+
+
+
+ this.onChangeSSHConfigKey(i, e)} /> +
+
+ this.onChangeSSHConfigValue(i, e)} /> +
+
+
+
+ ))} + + {this.isCreate() ? null : (<> +
+ +
Delete host
+ +
+
+ +
+
This cannot be undone.
+
+ )} +
+
{/* INFO hpello .buttons modifier is-right does not seem to work without block style */} +
+ + +
+
+
+
+ ) + } +} + +const mapStateToProps = (state: State, ownProps: OwnProps): Props => ({ + ...ownProps, + hosts: state.api.state.hosts +}) + +export default connect(mapStateToProps)(HostForm) diff --git a/gui/src/components/SystemInfo.tsx b/gui/src/components/SystemInfo.tsx new file mode 100644 index 0000000..3cc5f53 --- /dev/null +++ b/gui/src/components/SystemInfo.tsx @@ -0,0 +1,121 @@ +import * as React from 'react' +import { connect } from 'react-redux' + +import { State } from '../types/redux' + +import { SystemState } from '../../../server/src/system/reducer' +import { SystemInfo as SystemInfoType } from '../../../server/src/system/types' + +const largesp = +const thinnbsp = + +const formatDuration = (secs: number): React.ReactNode => { + if (secs < 60) { return <><{thinnbsp}1{thinnbsp}min } + + let seconds = secs + const weeks = Math.floor(seconds / (7 * 24 * 60 * 60)) + if (weeks > 0) { seconds -= weeks * 7 * 24 * 60 * 60 } + + const days = Math.floor(seconds / (24 * 60 * 60)) + if (days > 0) { seconds -= days * 24 * 60 * 60 } + + const hours = Math.floor(seconds / (60 * 60)) + if (hours > 0) { seconds -= hours * 60 * 60 } + + const minutes = Math.floor(seconds / 60) + + return (<> + {weeks > 0 ? <>{weeks}{thinnbsp}w : null} + {days > 0 ? <>{days}{thinnbsp}d : null} + {hours > 0 ? <>{hours}{thinnbsp}h : null} + {minutes}{thinnbsp}min + ) +} + +const formatMemory = (bytes: number): React.ReactNode => { + const g = bytes / (2 ** 30) + if (Math.floor(g * 10) > 0) { + return <>{(bytes / (2 ** 30)).toFixed(2)}{thinnbsp}GB + } + + const m = bytes / (2 ** 20) + if (Math.floor(m * 10) > 0) { + return <>{(bytes / (2 ** 20)).toFixed(2)}{thinnbsp}MB + } + + return <>{(bytes / (2 ** 10)).toFixed(2)}{thinnbsp}kB +} + +const formatPercentage = (value: number): React.ReactNode => { + return <>{value.toFixed(2)}{thinnbsp}% +} + +const makeSystemSummary = (info: SystemInfoType): React.ReactNode => { + return (<> + {info.platform} ({info.arch}){largesp}|{largesp} + {info.totalCPUs}{thinnbsp}CPU{info.totalCPUs > 1 ? 's' : ''}{largesp}|{largesp} + {formatMemory(info.totalMemoryBytes)} + ) +} + +interface OwnProps {} + +interface StateProps { + system: SystemState +} + +interface Props extends StateProps, OwnProps { } + +class SystemInfo extends React.Component { + render() { + const { info, stats } = this.props.system + + return ( +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
System + {info ? makeSystemSummary(info) : ''} +
Version + {info ? <>sshmon {info.version}{largesp}|{largesp}Node.js {info.nodeVersion} : ''} +
Uptime + {stats ? formatDuration(stats.uptimeSeconds) : ''} +
CPU usage + {stats ? formatPercentage(stats.cpuUsage) : ''} +
Memory usage + {stats ? formatMemory(stats.memoryUsageBytes) : ''} +
+
+
+ ) + } +} + +const mapStateToProps = (state: State, ownProps: OwnProps): Props => ({ + ...ownProps, + system: state.api.state.system +}) + +export default connect(mapStateToProps)(SystemInfo) diff --git a/gui/src/index.tsx b/gui/src/index.tsx new file mode 100644 index 0000000..a3de323 --- /dev/null +++ b/gui/src/index.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import { Provider } from 'react-redux' + +import App from './components/App' +import { store } from './store' +import { APIClient } from './api/client' + +const apiClient = new APIClient(store) + +const Index = ( + + + +) + +ReactDOM.render(Index, document.getElementById('root')) diff --git a/gui/src/reducer.ts b/gui/src/reducer.ts new file mode 100644 index 0000000..d6a879e --- /dev/null +++ b/gui/src/reducer.ts @@ -0,0 +1,16 @@ +import { combineReducers } from 'redux' + +import { reducer as api, State as APIState } from './api/reducer' +import { Action as APIAction } from './api/actions' + +export type State = { + api: APIState +} + +export type Action = + | APIAction + | { type: '__any_other_action_type__' } + +export default combineReducers({ + api +} as any) // FIXME hpello https://github.com/reactjs/redux/issues/2709 diff --git a/gui/src/store.ts b/gui/src/store.ts new file mode 100644 index 0000000..6aa1e17 --- /dev/null +++ b/gui/src/store.ts @@ -0,0 +1,20 @@ +import { applyMiddleware, createStore } from 'redux' +import { createLogger } from 'redux-logger' + +import reducer from './reducer' + +const makeMiddleware = () => { + return applyMiddleware(createLogger({ + level: { + prevState: false, + action: 'log', + nextState: false, + error: 'error' + } + })) +} + +export const store = createStore( + reducer, + makeMiddleware() +) diff --git a/gui/src/types/redux.d.ts b/gui/src/types/redux.d.ts new file mode 100644 index 0000000..7f7454c --- /dev/null +++ b/gui/src/types/redux.d.ts @@ -0,0 +1,8 @@ +import { Store as ReduxStore } from 'redux' + +import { Action as _Action, State as _State } from '../reducer' + +export type Action = _Action +export type State = _State + +export type Store = ReduxStore diff --git a/gui/tsconfig.json b/gui/tsconfig.json new file mode 100644 index 0000000..902306b --- /dev/null +++ b/gui/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "src" + ] +} diff --git a/gui/tslint.yml b/gui/tslint.yml new file mode 100644 index 0000000..eaa88e6 --- /dev/null +++ b/gui/tslint.yml @@ -0,0 +1,4 @@ +extends: + - ../tslint.yml +rules: + no-console: false diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ebc7cbb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1164 @@ +{ + "name": "sshmon", + "version": "0.3.14", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "acorn": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", + "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==" + }, + "acorn-object-rest-spread": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/acorn-object-rest-spread/-/acorn-object-rest-spread-1.1.0.tgz", + "integrity": "sha1-eGma790Y7DGCyq2t9S4ml8BI9HY=", + "requires": { + "acorn": "5.5.3" + } + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "requires": { + "color-convert": "1.9.1" + } + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "babel-runtime": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.25.0.tgz", + "integrity": "sha1-M7mOql1IK7AajRqmtDetKwGuxBw=", + "requires": { + "core-js": "2.5.5", + "regenerator-runtime": "0.10.5" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.9.tgz", + "integrity": "sha512-/+o3o6OV1cm3WKrO7U4wykU+ZICE6HiMEuravc2d03NIuM/VaRn5iMcoQ7NyxFXjvpmRICP2EER0YOnh4yIapA==", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", + "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "core-js": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.5.tgz", + "integrity": "sha1-sU3ek2xkDAV5prUMq8wTLdYSfjs=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "diff": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz", + "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==", + "dev": true + }, + "doctrine": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz", + "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=", + "dev": true, + "requires": { + "esutils": "1.1.6", + "isarray": "0.0.1" + }, + "dependencies": { + "esutils": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz", + "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "requires": { + "esprima": "2.7.3", + "estraverse": "1.9.3", + "esutils": "2.0.2", + "optionator": "0.8.2", + "source-map": "0.2.0" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "expand-template": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-1.0.3.tgz", + "integrity": "sha1-bDAzIxd6YrGyLAcCefeGEoe2mxo=" + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "fs-extra": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.1.tgz", + "integrity": "sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA=", + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "3.0.1", + "universalify": "0.1.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "requires": { + "array-union": "1.0.2", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.14.1" + } + }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "requires": { + "graceful-fs": "4.1.11" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.9" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "multistream": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-2.1.0.tgz", + "integrity": "sha1-YlwmfVxEQkrWKUeItbtNo9yzLx0=", + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.6" + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/pkg/-/pkg-4.3.1.tgz", + "integrity": "sha512-QaOXdF9doVkrXpeu0D5ODLDLjYE4LE2WAk7/wSgNiCsCajg4ExjApxwkVIanz61tR8oIe+8vkmW0WpAwfV1ExA==", + "requires": { + "acorn": "5.5.3", + "acorn-object-rest-spread": "1.1.0", + "babel-runtime": "6.25.0", + "chalk": "2.1.0", + "escodegen": "1.8.1", + "fs-extra": "4.0.1", + "globby": "6.1.0", + "minimist": "1.2.0", + "multistream": "2.1.0", + "pkg-fetch": "2.5.4", + "progress": "2.0.0", + "resolve": "1.4.0", + "simple-bufferstream": "1.0.0", + "stream-meter": "1.0.4" + } + }, + "pkg-fetch": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-2.5.4.tgz", + "integrity": "sha512-KASiP5yytve4otDY242Zp3r+e11whyoSl79QmmBS3Qg4rvZsYOC5RE0szM0SZrVxg93sqYcINxHlXmzBTJDOeA==", + "requires": { + "babel-runtime": "6.25.0", + "byline": "5.0.0", + "chalk": "2.1.0", + "expand-template": "1.0.3", + "fs-extra": "4.0.1", + "in-publish": "2.0.0", + "minimist": "1.2.0", + "progress": "2.0.0", + "request": "2.81.0", + "request-progress": "3.0.0", + "semver": "5.4.1", + "unique-temp-dir": "1.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.2", + "stringstream": "0.0.5", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + } + }, + "request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", + "requires": { + "throttleit": "1.0.0" + } + }, + "resolve": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", + "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", + "requires": { + "path-parse": "1.0.5" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + }, + "simple-bufferstream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-bufferstream/-/simple-bufferstream-1.0.0.tgz", + "integrity": "sha1-XKsQ6FGqccZnt3th/hux2QpguqQ=" + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "requires": { + "hoek": "2.16.3" + } + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "optional": true, + "requires": { + "amdefine": "1.0.1" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", + "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "stream-meter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", + "integrity": "sha1-Uq+Vql6nYKJJFxZwTb/5D3Ov3R0=", + "requires": { + "readable-stream": "2.3.6" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "requires": { + "has-flag": "2.0.0" + } + }, + "throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=" + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "1.4.1" + } + }, + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", + "dev": true + }, + "tslint": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz", + "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "builtin-modules": "1.1.1", + "chalk": "2.3.0", + "commander": "2.14.1", + "diff": "3.4.0", + "glob": "7.1.2", + "js-yaml": "3.10.0", + "minimatch": "3.0.4", + "resolve": "1.4.0", + "semver": "5.4.1", + "tslib": "1.9.0", + "tsutils": "2.21.0" + }, + "dependencies": { + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + } + } + }, + "tslint-config-airbnb": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/tslint-config-airbnb/-/tslint-config-airbnb-5.6.0.tgz", + "integrity": "sha512-NtGczet8KAWD5SQrTLoX70fsR82tzS71UG+Zn3RQjkbAanqba2I1OIKV6zyBBqQblnujsbuKaIgrF1IAKNlb/w==", + "dev": true, + "requires": { + "tslint-consistent-codestyle": "1.11.1", + "tslint-eslint-rules": "4.1.1", + "tslint-microsoft-contrib": "5.0.2" + } + }, + "tslint-consistent-codestyle": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslint-consistent-codestyle/-/tslint-consistent-codestyle-1.11.1.tgz", + "integrity": "sha512-wLu+Ct8x4mBmVkuhEiNAnUBkxchMV2Le0ikBsST5HnKbGlm3K4RSpXCBSI1VtJDk748W2I5hDzgsInawLdnxwQ==", + "dev": true, + "requires": { + "tslib": "1.9.0", + "tsutils": "2.21.0" + } + }, + "tslint-eslint-rules": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tslint-eslint-rules/-/tslint-eslint-rules-4.1.1.tgz", + "integrity": "sha1-fDDniC8mvCdr/5HSOEl1xp2viLo=", + "dev": true, + "requires": { + "doctrine": "0.7.2", + "tslib": "1.9.0", + "tsutils": "1.9.1" + }, + "dependencies": { + "tsutils": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-1.9.1.tgz", + "integrity": "sha1-ufmrROVa+WgYMdXyjQrur1x1DLA=", + "dev": true + } + } + }, + "tslint-microsoft-contrib": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/tslint-microsoft-contrib/-/tslint-microsoft-contrib-5.0.2.tgz", + "integrity": "sha1-7MKnl/d3oS8AZpRM7AyBqefFnuk=", + "dev": true, + "requires": { + "tsutils": "2.21.0" + } + }, + "tsutils": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.21.0.tgz", + "integrity": "sha512-zlOHTYtTwvTiKxUyAU8wiKzPpAgwZrGjb7AY18VUlxuCgBiTMVorIl5HjrCT8V64Hm34RI1BZITJMVQpBLMxVg==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "1.1.2" + } + }, + "typescript": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.1.tgz", + "integrity": "sha512-bqB1yS6o9TNA9ZC/MJxM0FZzPnZdtHj0xWK/IZ5khzVqdpGul/R/EIiHRgFXlwTD7PSIaYVnGKq1QgMCu2mnqw==", + "dev": true + }, + "uid2": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", + "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" + }, + "unique-temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz", + "integrity": "sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U=", + "requires": { + "mkdirp": "0.5.1", + "os-tmpdir": "1.0.2", + "uid2": "0.0.3" + } + }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cef2324 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "sshmon", + "version": "0.3.14", + "scripts": { + "build": "(cd server && npm run build) && (cd gui && npm run build)", + "build-watch": "(cd server && npm run build-watch) & (cd gui && npm run build-watch)", + "clean": "(cd server && npm run clean) && (cd gui && npm run clean)", + "tsc": "tsc --outDir /tmp/none", + "tsc-watch": "npm run tsc -- --watch", + "install-all": "npm install & (cd server && npm install) && (cd gui && npm install)", + "prune-all": "npm prune --production && (cd server && npm prune --production) && (cd gui && npm prune --production)", + "start": "(cd server && npm run start)", + "pkg": "mkdir -p build && pkg . -o build/sshmon", + "pkg-clean": "! test -e build || rm -r build", + "deploy": "npm run pkg-clean && npm run clean && npm run build && npm run pkg" + }, + "devDependencies": { + "tslint": "^5.9.1", + "tslint-config-airbnb": "^5.6.0", + "typescript": "^2.7.1" + }, + "bin": "server/build/cli.js", + "pkg": { + "assets": [ + "gui/public", + "gui/node_modules/font-awesome/fonts" + ] + }, + "dependencies": { + "pkg": "^4.3.1" + } +} diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..7b9f68b --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,2268 @@ +{ + "name": "sshmon-server", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@types/bunyan": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.4.tgz", + "integrity": "sha512-bxOF3fsm69ezKxdcJ7Oo/PsZMOJ+JIV/QJO2IADfScmR3sLulR88dpSnz6+q+9JJ1kD7dXFFgUrGRSKHLkOX7w==", + "dev": true, + "requires": { + "@types/events": "1.1.0", + "@types/node": "8.9.1" + } + }, + "@types/chokidar": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@types/chokidar/-/chokidar-1.7.4.tgz", + "integrity": "sha512-QVuksEzbvU22DJg9vFW9O++u0yT6aXnn64qq/KzaUUWuf+E2IAEzAM90qGlgWpLq3pPHbUfvmlJz1TZnUePQUg==", + "dev": true, + "requires": { + "@types/events": "1.1.0", + "@types/node": "8.9.1" + } + }, + "@types/events": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-1.1.0.tgz", + "integrity": "sha512-y3bR98mzYOo0pAZuiLari+cQyiKk3UXRuT45h1RjhfeCzqkjaVsfZJNaxdgtk7/3tzOm1ozLTqEqMP3VbI48jw==", + "dev": true + }, + "@types/http-proxy": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.12.4.tgz", + "integrity": "sha512-QoJzW1//JECNrhz+P1R9QczqtIfEJ1UiFXMb++dy9bQ7UvpIlCf9XLvo9JrXFQdUxv5Xj0LB3snwqE6R1AlFUw==", + "dev": true, + "requires": { + "@types/events": "1.1.0", + "@types/node": "8.9.1" + } + }, + "@types/joi": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@types/joi/-/joi-13.0.5.tgz", + "integrity": "sha512-xhGKDKk8qEK35GFYIkpQXdS03PnL+1eXt8VOHnuvuMOjNCztmTpqiDk2vlDy3GbSctSTBJCkY0PXkaRVJpl2xA==", + "dev": true + }, + "@types/js-yaml": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.10.1.tgz", + "integrity": "sha512-IpKg0KGIUNcydttaGURhSLrq1eSNoSjN7T1MokAuasIPBKzsHxcz3MAdFGzasmYQVWf6XxG+jQTJ9UFOL29Ubg==", + "dev": true + }, + "@types/json-stable-stringify": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.32.tgz", + "integrity": "sha512-q9Q6+eUEGwQkv4Sbst3J4PNgDOvpuVuKj79Hl/qnmBMEIPzB5QoFRUtjcgcg2xNUZyYUGXBk5wYIBKHt0A+Mxw==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.102", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.102.tgz", + "integrity": "sha512-k/SxycYmVc6sYo6kzm8cABHcbMs9MXn6jYsja1hLvZ/x9e31VHRRn+1UzWdpv6doVchphvKaOsZ0VTqbF7zvNg==", + "dev": true + }, + "@types/node": { + "version": "8.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.1.tgz", + "integrity": "sha512-4JFGIC1RSoFngVsT5EZcL793/uRi/OJ3ilsp9DQUr4LZOaMhNM1pPrt9TqlXOnXj3h73hl6NF31v87eQAPXYTg==", + "dev": true + }, + "@types/restify": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/restify/-/restify-5.0.7.tgz", + "integrity": "sha512-0bcMA32Ys6nOQnD4QD6vDvfJg7nx5Dbd+oItNFAad3lwnanm0CqxSZpPQVgVMdD4Vrq/dY7yTaEUwsXOFci2iw==", + "dev": true, + "requires": { + "@types/bunyan": "1.8.4", + "@types/node": "8.9.1", + "@types/spdy": "3.4.4" + } + }, + "@types/socket.io": { + "version": "1.4.31", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-1.4.31.tgz", + "integrity": "sha512-HnO78rKiAx+tCEKYFurJvKLNAluN9B0V+yLnkJEY9BVwmCXHA7oKeLCymqP/uAgx0t+un/JM+GJOTwNQJ+ODvQ==", + "dev": true, + "requires": { + "@types/node": "8.9.1" + } + }, + "@types/spdy": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@types/spdy/-/spdy-3.4.4.tgz", + "integrity": "sha512-N9LBlbVRRYq6HgYpPkqQc3a9HJ/iEtVZToW6xlTtJiMhmRJ7jJdV7TaZQJw/Ve/1ePUsQiCTDc4JMuzzag94GA==", + "dev": true, + "requires": { + "@types/node": "8.9.1" + } + }, + "@types/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", + "dev": true + }, + "@types/yargs": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-11.0.0.tgz", + "integrity": "sha512-vFql3tOxs6clgh+WVoLW3nOkNGCdeKsMU6mQZkOerJpV/CR9Xc1c1lZ+kYU+hNSobrQIOcNovWfPFDJIhcG5Pw==", + "dev": true + }, + "accepts": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=", + "requires": { + "mime-types": "2.1.17", + "negotiator": "0.6.1" + } + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "requires": { + "sprintf-js": "1.0.3" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=" + }, + "blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=" + }, + "brace-expansion": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.9.tgz", + "integrity": "sha512-/+o3o6OV1cm3WKrO7U4wykU+ZICE6HiMEuravc2d03NIuM/VaRn5iMcoQ7NyxFXjvpmRICP2EER0YOnh4yIapA==", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "bunyan": { + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", + "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=", + "requires": { + "dtrace-provider": "0.8.6", + "moment": "2.20.1", + "mv": "2.1.1", + "safe-json-stringify": "1.0.4" + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "fsevents": "1.2.3", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + } + }, + "cliui": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.0.0.tgz", + "integrity": "sha512-nY3W5Gu2racvdDk//ELReY+dHjb9PlIcVDFXP72nVIhq2Gy3LuVXYwJoPVudwQnv1shtohpgkdCKT2YaKY0CKw==", + "requires": { + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "wrap-ansi": "2.1.0" + } + }, + "clone-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-1.0.0.tgz", + "integrity": "sha1-6uCiQT9VwJQvgYwin+/OhF1/Oxw=", + "requires": { + "is-regexp": "1.0.0", + "is-supported-regexp-flag": "1.0.0" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "4.1.1", + "shebang-command": "1.2.0", + "which": "1.3.0" + } + }, + "csv": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/csv/-/csv-1.2.1.tgz", + "integrity": "sha1-UjHt/BxxUlEuxFeBB2p6l/9SXAw=", + "requires": { + "csv-generate": "1.1.2", + "csv-parse": "1.3.3", + "csv-stringify": "1.1.2", + "stream-transform": "0.2.2" + } + }, + "csv-generate": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-1.1.2.tgz", + "integrity": "sha1-7GsA7a7W5ZrZwgWC9MNk4osUYkA=" + }, + "csv-parse": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-1.3.3.tgz", + "integrity": "sha1-0c/YdDwvhJoKuy/VRNtWaV0ZpJA=" + }, + "csv-stringify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-1.1.2.tgz", + "integrity": "sha1-d6QVJlgbzjOA8SsA18W7rHDIK1g=", + "requires": { + "lodash.get": "4.4.2" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "detect-node": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.3.tgz", + "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=" + }, + "dtrace-provider": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.6.tgz", + "integrity": "sha1-QooiOv4DQl0s1tY0f99AxmkDVj0=", + "optional": true, + "requires": { + "nan": "2.8.0" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "engine.io": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.1.4.tgz", + "integrity": "sha1-PQIRtwpVLOhB/8fahiezAamkFi4=", + "requires": { + "accepts": "1.3.3", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "2.6.9", + "engine.io-parser": "2.1.2", + "uws": "0.14.5", + "ws": "3.3.3" + } + }, + "engine.io-client": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.1.4.tgz", + "integrity": "sha1-T88TcLRxY70s6b4nM5ckMDUNTqE=", + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "2.6.9", + "engine.io-parser": "2.1.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "3.3.3", + "xmlhttprequest-ssl": "1.5.5", + "yeast": "0.1.2" + } + }, + "engine.io-parser": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.2.tgz", + "integrity": "sha512-dInLFzr80RijZ1rGpx1+56/uFoH7/7InhH3kZt+Ms6hT8tNx3NGW/WNSA/f8As1WkOfkuyb3tnRyuXGxusclMw==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.4", + "has-binary2": "1.0.2" + } + }, + "escape-regexp-component": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/escape-regexp-component/-/escape-regexp-component-1.0.2.tgz", + "integrity": "sha1-nGO20LJf8qiMOtvRjFthrMO5+qI=" + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" + }, + "eventemitter3": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", + "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=" + }, + "ewma": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ewma/-/ewma-2.0.1.tgz", + "integrity": "sha512-MYYK17A76cuuyvkR7MnqLW4iFYPEi5Isl2qb8rXiWpLiwFS9dxW/rncuNnjjgSENuVqZQkIuR4+DChVL4g1lnw==", + "requires": { + "assert-plus": "1.0.0" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "requires": { + "fill-range": "2.2.3" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "1.0.0" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "2.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "requires": { + "for-in": "1.0.2" + } + }, + "formidable": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.1.1.tgz", + "integrity": "sha1-lriIb3w8NQi5Mta9cMTTqI818ak=" + }, + "fsevents": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.3.tgz", + "integrity": "sha512-X+57O5YkDTiEQGiw8i7wYc2nQgweIekqkepI8Q3y4wVlurgBt2SuwxTeYUYMZIGpLZH3r/TsMjczCMXE5ZOt7Q==", + "optional": true, + "requires": { + "nan": "2.10.0", + "node-pre-gyp": "0.9.1" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "optional": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": "2.1.2" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "minimatch": "3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "optional": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "requires": { + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "optional": true + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "2.6.9", + "iconv-lite": "0.4.21", + "sax": "1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.9.1", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "1.0.3", + "mkdirp": "0.5.1", + "needle": "2.2.0", + "nopt": "4.0.1", + "npm-packlist": "1.1.10", + "npmlog": "4.1.2", + "rc": "1.2.6", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "4.4.1" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.5" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "optional": true, + "requires": { + "ignore-walk": "3.0.1", + "npm-bundled": "1.0.3" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.6", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "optional": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "optional": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "bundled": true, + "optional": true, + "requires": { + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.2.4", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true + } + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "2.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "handle-thing": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", + "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=" + }, + "has-binary2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.2.tgz", + "integrity": "sha1-6D26SfC5vk0CbSc2U1DZ8D9Uvpg=", + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, + "hoek": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.3.tgz", + "integrity": "sha512-Bmr56pxML1c9kU+NS51SMFkiVQAb+9uFfXwyqR2tn4w2FPvmPt65eZ9aCcEfRXd9G74HkZnILC6p967pED4aiw==" + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "requires": { + "inherits": "2.0.3", + "obuf": "1.1.1", + "readable-stream": "2.3.3", + "wbuf": "1.7.2" + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" + }, + "http-proxy": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.16.2.tgz", + "integrity": "sha1-Bt/ykpUr9k2+hHH6nfcwZtTzd0I=", + "requires": { + "eventemitter3": "1.2.0", + "requires-port": "1.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "optional": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "1.11.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "requires": { + "kind-of": "3.2.2" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" + }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-supported-regexp-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-supported-regexp-flag/-/is-supported-regexp-flag-1.0.0.tgz", + "integrity": "sha1-i1IMhfrnolM4LUsCZS4EVXbhO7g=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isemail": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.1.1.tgz", + "integrity": "sha512-mVjAjvdPkpwXW61agT2E9AkGoegZO7SdJGCezWwxnETL58f5KwJ4vSVAMBUL5idL6rTlYAIGkX3n4suiviMLNw==", + "requires": { + "punycode": "2.1.0" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + }, + "joi": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-13.1.2.tgz", + "integrity": "sha512-bZZSQYW5lPXenOfENvgCBPb9+H6E6MeNWcMtikI04fKphj5tvFL9TOb+H2apJzbCrRw/jebjTH8z6IHLpBytGg==", + "requires": { + "hoek": "5.0.3", + "isemail": "3.1.1", + "topo": "3.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "0.0.0" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "1.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + }, + "lodash-es": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.5.tgz", + "integrity": "sha512-Ez3ONp3TK9gX1HYKp6IhetcVybD+2F+Yp6GS9dfH8ue6EOCEzQtQEh4K0FYWBP9qLv+lzeQAYXw+3ySfxyZqkw==" + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "requires": { + "js-tokens": "3.0.2" + } + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "requires": { + "mimic-fn": "1.2.0" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "requires": { + "mime-db": "1.30.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "minimalistic-assert": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.9" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "moment": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", + "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==", + "optional": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "requires": { + "mkdirp": "0.5.1", + "ncp": "2.0.0", + "rimraf": "2.4.5" + } + }, + "nan": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", + "optional": true + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "2.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "obuf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.1.tgz", + "integrity": "sha1-EEEktsYCxnlogaBCVB0220OlJk4=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-limit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "1.2.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "1.0.2" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "1.0.2" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "pidusage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-1.2.0.tgz", + "integrity": "sha512-OGo+iSOk44HRJ8q15AyG570UYxcm5u+R99DI8Khu8P3tKGkVu5EZX4ywHglWSTMNNXQ274oeGpYrvFEhDIFGPg==" + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "punycode": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", + "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.3", + "set-immediate-shim": "1.0.1" + } + }, + "redux": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", + "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", + "requires": { + "lodash": "4.17.5", + "lodash-es": "4.17.5", + "loose-envify": "1.3.1", + "symbol-observable": "1.2.0" + } + }, + "redux-thunk": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.2.0.tgz", + "integrity": "sha1-5hWhbha0ehmlFXZhM9Hj6Zt4UuU=" + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, + "restify": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/restify/-/restify-6.3.4.tgz", + "integrity": "sha512-kYmXwCtj7gJ6e7vMwTjkFkygMTdcxwbJGJ+JXdsREMLx0h23mVBwLpyi2tqgI6DE1S0Al7//y7gbLXP79RqKaw==", + "requires": { + "assert-plus": "1.0.0", + "bunyan": "1.8.12", + "clone-regexp": "1.0.0", + "csv": "1.2.1", + "dtrace-provider": "0.8.6", + "escape-regexp-component": "1.0.2", + "ewma": "2.0.1", + "formidable": "1.1.1", + "http-signature": "1.2.0", + "lodash": "4.17.5", + "lru-cache": "4.1.1", + "mime": "1.6.0", + "negotiator": "0.6.1", + "once": "1.4.0", + "pidusage": "1.2.0", + "qs": "6.5.1", + "restify-errors": "5.0.0", + "semver": "5.5.0", + "spdy": "3.4.7", + "uuid": "3.2.1", + "vasync": "1.6.4", + "verror": "1.10.0" + } + }, + "restify-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/restify-errors/-/restify-errors-5.0.0.tgz", + "integrity": "sha512-+vby9Kxf7qlzvbZSTIEGkIixkeHG+pVCl34dk6eKnL+ua4pCezpdLT/1/eabzPZb65ADrgoc04jeWrrF1E1pvQ==", + "requires": { + "assert-plus": "1.0.0", + "lodash": "4.17.5", + "safe-json-stringify": "1.0.4", + "verror": "1.10.0" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "6.0.4" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "safe-json-stringify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz", + "integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=", + "optional": true + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "socket.io": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.0.4.tgz", + "integrity": "sha1-waRZDO/4fs8TxyZS8Eb3FrKeYBQ=", + "requires": { + "debug": "2.6.9", + "engine.io": "3.1.4", + "socket.io-adapter": "1.1.1", + "socket.io-client": "2.0.4", + "socket.io-parser": "3.1.2" + } + }, + "socket.io-adapter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", + "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" + }, + "socket.io-client": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.0.4.tgz", + "integrity": "sha1-CRilUkBtxeVAs4Dc2Xr8SmQzL44=", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "2.6.9", + "engine.io-client": "3.1.4", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "3.1.2", + "to-array": "0.1.4" + } + }, + "socket.io-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.2.tgz", + "integrity": "sha1-28IoIVH8T6675Aru3Ady66YZ9/I=", + "requires": { + "component-emitter": "1.2.1", + "debug": "2.6.9", + "has-binary2": "1.0.2", + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, + "spdy": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", + "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", + "requires": { + "debug": "2.6.9", + "handle-thing": "1.2.5", + "http-deceiver": "1.2.7", + "safe-buffer": "5.1.1", + "select-hose": "2.0.0", + "spdy-transport": "2.0.20" + } + }, + "spdy-transport": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.0.20.tgz", + "integrity": "sha1-c15yBUxIayNU/onnAiVgBKOazk0=", + "requires": { + "debug": "2.6.9", + "detect-node": "2.0.3", + "hpack.js": "2.1.6", + "obuf": "1.1.1", + "readable-stream": "2.3.3", + "safe-buffer": "5.1.1", + "wbuf": "1.7.2" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, + "stream-transform": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-0.2.2.tgz", + "integrity": "sha1-dYZ0h/SVKPi/HYJJllh1PQLfeDg=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "3.0.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, + "topo": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.0.tgz", + "integrity": "sha512-Tlu1fGlR90iCdIPURqPiufqAlCZYzLjHYVVbcFWDMcX7+tK8hdZWAfsMrD/pBul9jqHHwFjNdf1WaxA9vTRRhw==", + "requires": { + "hoek": "5.0.3" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "typescript": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.1.tgz", + "integrity": "sha512-bqB1yS6o9TNA9ZC/MJxM0FZzPnZdtHj0xWK/IZ5khzVqdpGul/R/EIiHRgFXlwTD7PSIaYVnGKq1QgMCu2mnqw==", + "dev": true + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "uws": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/uws/-/uws-0.14.5.tgz", + "integrity": "sha1-Z6rzPEaypYel9mZtAPdpEyjxSdw=", + "optional": true + }, + "vasync": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz", + "integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=", + "requires": { + "verror": "1.6.0" + }, + "dependencies": { + "extsprintf": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz", + "integrity": "sha1-WtlGwi9bMrp/jNdCZxHG6KP8JSk=" + }, + "verror": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz", + "integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=", + "requires": { + "extsprintf": "1.2.0" + } + } + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "wbuf": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.2.tgz", + "integrity": "sha1-1pe5nx9ZUS3ydRvkJ2nBWAtYAf4=", + "requires": { + "minimalistic-assert": "1.0.0" + } + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.1", + "ultron": "1.1.1" + } + }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yargs": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.0.0.tgz", + "integrity": "sha512-Rjp+lMYQOWtgqojx1dEWorjCofi1YN7AoFvYV7b1gx/7dAAeuI4kN5SZiEvr0ZmsZTOpDRcCqrpI10L31tFkBw==", + "requires": { + "cliui": "4.0.0", + "decamelize": "1.2.0", + "find-up": "2.1.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "9.0.2" + } + }, + "yargs-parser": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", + "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "requires": { + "camelcase": "4.1.0" + } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..206b7bb --- /dev/null +++ b/server/package.json @@ -0,0 +1,39 @@ +{ + "name": "sshmon-server", + "version": "", + "scripts": { + "build": "tsc --outDir build", + "build-watch": "npm run build -- --watch | ruby -ne 'puts $_.gsub(/\\x1Bc/, \"\")'", + "clean": "! test -e build || rm -r build", + "lint": "../node_modules/.bin/tslint -p .", + "start": "node build/cli.js" + }, + "dependencies": { + "bunyan": "^1.8.12", + "chokidar": "^1.7.0", + "http-proxy": "^1.16.2", + "joi": "^13.0.2", + "js-yaml": "^3.10.0", + "json-stable-stringify": "^1.0.1", + "lodash": "^4.17.4", + "redux": "^3.7.2", + "redux-thunk": "^2.2.0", + "restify": "^6.3.4", + "socket.io": "^2.0.4", + "tmp": "0.0.33", + "yargs": "^11.0.0" + }, + "devDependencies": { + "@types/chokidar": "^1.7.4", + "@types/http-proxy": "^1.12.3", + "@types/joi": "^13.0.5", + "@types/js-yaml": "^3.10.1", + "@types/json-stable-stringify": "^1.0.32", + "@types/lodash": "^4.14.101", + "@types/restify": "^5.0.7", + "@types/socket.io": "^1.4.31", + "@types/tmp": "0.0.33", + "@types/yargs": "^11.0.0", + "typescript": "^2.7.1" + } +} diff --git a/server/src/api/api-server.ts b/server/src/api/api-server.ts new file mode 100644 index 0000000..1514448 --- /dev/null +++ b/server/src/api/api-server.ts @@ -0,0 +1,12 @@ +import { createServer as createStaticServer } from './static-server' +import { SocketNotify } from './socket-notify' +import { createIO } from './socket-server' +import { Store } from '../types/redux' + +export const createServer = (store: Store, socketNotify: SocketNotify) => { + const server = createStaticServer() + const io = createIO(store, socketNotify) + io.attach(server.server) + + return server +} diff --git a/server/src/api/api.ts b/server/src/api/api.ts new file mode 100644 index 0000000..50da346 --- /dev/null +++ b/server/src/api/api.ts @@ -0,0 +1,31 @@ +import { AutoconnectConfig } from '../autoconnect' +import { AutoforwardConfig } from '../autoforward' +import { HostConfig } from '../host' +import { ForwardingConfig } from '../forward' + +export enum apiKeys { + hostCreate = 'hostCreate', + hostEdit = 'hostEdit', + hostDelete = 'hostDelete', + hostConnect = 'hostConnect', + hostDisconnect = 'hostDisconnect', + + forwardingCreate = 'forwardingCreate', + forwardingEdit = 'forwardingEdit', + forwardingDelete = 'forwardingDelete', + forwardingConnect = 'forwardingConnect', + forwardingDisconnect = 'forwardingDisconnect' +} + +export type APIEndpoint = + | { key: apiKeys.hostCreate, args: { id: string, config: HostConfig, autoConfig: AutoconnectConfig } } + | { key: apiKeys.hostEdit, args: { id: string, config: HostConfig, autoConfig: AutoconnectConfig } } + | { key: apiKeys.hostDelete, args: { id: string } } + | { key: apiKeys.hostConnect, args: { id: string } } + | { key: apiKeys.hostDisconnect, args: { id: string } } + + | { key: apiKeys.forwardingCreate, args: { id: string, fwdId: string, config: ForwardingConfig, autoConfig: AutoforwardConfig } } + | { key: apiKeys.forwardingEdit, args: { id: string, fwdId: string, config: ForwardingConfig, autoConfig: AutoforwardConfig } } + | { key: apiKeys.forwardingDelete, args: { id: string, fwdId: string } } + | { key: apiKeys.forwardingConnect, args: { id: string, fwdId: string } } + | { key: apiKeys.forwardingDisconnect, args: { id: string, fwdId: string } } diff --git a/server/src/api/component.ts b/server/src/api/component.ts new file mode 100644 index 0000000..d7da0d1 --- /dev/null +++ b/server/src/api/component.ts @@ -0,0 +1,65 @@ +import { Server } from 'http' +import { promisify } from 'util' + +import { createServer as createAPIServer } from './api-server' +import { createServer as createGatewayServer, GatewayServer } from './gateway-server' +import { ProxyTarget } from './simple-proxy-server' +import { SocketNotify } from './socket-notify' +import { State, Store } from '../types/redux' +import { onStateChange } from './utils' +import { createLogger } from '../utils/log' +import { formatURL } from '../utils/server-url' +import { makeTmpPath } from '../utils/tmp' + +const log = createLogger(__filename) + +export class API { + store: Store + prevState: State + apiServer: Server + gatewayServer: GatewayServer + socketNotify: SocketNotify + + constructor(params: { store: Store }) { + this.store = params.store + const state = params.store.getState() + + this.prevState = state + this.socketNotify = new SocketNotify(state) + this.apiServer = createAPIServer(params.store, this.socketNotify) + this.gatewayServer = createGatewayServer() + } + + setup() { + this.store.subscribe(() => { + const state = this.store.getState() + + process.nextTick(() => { // prevent recursion + onStateChange( + this.prevState, + state, + (id: string, fwdId: string, target: ProxyTarget) => this.gatewayServer.addForwardingProxy(id, fwdId, target), + (id: string, fwdId: string) => this.gatewayServer.removeForwardingProxy(id, fwdId) + ) + + this.socketNotify.onStateChange(state) + + this.prevState = state + }) + }) + } + + async shutdown() { + await this.gatewayServer.shutdown() + } + + async listen(...args: any[]) { + const apiSocketPath = await makeTmpPath(__filename)('api-server') + await promisify(this.apiServer.listen).call(this.apiServer, apiSocketPath) + log.debug('api socket listening at %s', formatURL(this.apiServer)) + + this.gatewayServer.setDefaultTarget({ socketPath: apiSocketPath }) + await promisify(this.gatewayServer.listen).call(this.gatewayServer, ...args) + log.info('api listening at %s', formatURL(this.gatewayServer.server.server)) + } +} diff --git a/server/src/api/constants.ts b/server/src/api/constants.ts new file mode 100644 index 0000000..741b4dd --- /dev/null +++ b/server/src/api/constants.ts @@ -0,0 +1,16 @@ +export enum socketTypes { + // from client + register = 'register', + unregister = 'unregister', + apiCall = 'apiCall', + + // from server + state = 'state' +} + +export interface SocketMessageError { + message: string +} + +export const PROXY_PATH_PREFIX = '/proxy' +export const SOCKET_PATH = '/socket' diff --git a/server/src/api/gateway-server.ts b/server/src/api/gateway-server.ts new file mode 100644 index 0000000..5af0a3a --- /dev/null +++ b/server/src/api/gateway-server.ts @@ -0,0 +1,50 @@ +import { Socket } from 'net' + +import { PROXY_PATH_PREFIX } from './constants' +import { ProxyTarget, SimpleProxyServer } from './simple-proxy-server' + +const makeProxyPathPrefix = (pathPrefix: string, id: string, fwdId: string) => [pathPrefix, id, fwdId].join('/') + +export class GatewayServer { + server: SimpleProxyServer + pathPrefix: string + sockets: Set = new Set() + + constructor(pathPrefix: string) { + this.pathPrefix = pathPrefix + this.server = new SimpleProxyServer() + } + + setDefaultTarget(target: ProxyTarget) { + this.server.addTarget('/', target) + } + + addForwardingProxy(id: string, fwdId: string, target: ProxyTarget) { + const fullPrefix = makeProxyPathPrefix(this.pathPrefix, id, fwdId) + this.server.addTarget(fullPrefix, target) + } + + removeForwardingProxy(id: string, fwdId: string) { + const fullPrefix = makeProxyPathPrefix(this.pathPrefix, id, fwdId) + this.server.removeTarget(fullPrefix) + } + + listen(...args: any[]) { + this.server.server.listen(...args) + + // keep track of connected sockets for shutdown + this.server.server.on('connection', (socket) => { + this.sockets.add(socket) + socket.on('close', () => this.sockets.delete(socket)) + }) + } + + shutdown() { + return new Promise((resolve) => { + this.server.server.close(resolve) + this.sockets.forEach(socket => socket.destroy()) + }) + } +} + +export const createServer = () => new GatewayServer(PROXY_PATH_PREFIX) diff --git a/server/src/api/index.ts b/server/src/api/index.ts new file mode 100644 index 0000000..2e3d075 --- /dev/null +++ b/server/src/api/index.ts @@ -0,0 +1,2 @@ +export * from './component' +export * from './constants' diff --git a/server/src/api/simple-proxy-server.ts b/server/src/api/simple-proxy-server.ts new file mode 100644 index 0000000..712dae2 --- /dev/null +++ b/server/src/api/simple-proxy-server.ts @@ -0,0 +1,98 @@ +import { createServer as createHTTPServer, IncomingMessage, Server, ServerResponse } from 'http' +import * as HTTPProxy from 'http-proxy' +import { Socket } from 'net' + +export type ProxyTarget = + | { host: string, port: number } + | { socketPath: string } + +type ProxyConfig = { + pathPrefix: string, + proxyServer: HTTPProxy +} + +const handleRequest = (server: SimpleProxyServer) => (req: IncomingMessage, res: ServerResponse) => { + const { proxies } = server + const url = req.url as string // see IncomingMessage + + const proxy = proxies.find(p => url.startsWith(p.pathPrefix)) + if (!proxy) { + res.writeHead(404, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ code: 'NotFoundError', message: 'Not found' })) + return + } + + const { pathPrefix } = proxy + if (pathPrefix !== '/') { + if (!url.startsWith(`${pathPrefix}/`)) { + res.writeHead(302, { Location: `${pathPrefix.slice(pathPrefix.lastIndexOf('/') + 1)}/` }) + res.end() + return + } + + req.url = url.slice(pathPrefix.length) + } + + // @ts-ignore: web function requires nullable options + proxy.proxyServer.web(req, res, (err) => { + if (err.code === 'ECONNRESET') { + res.writeHead(502, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ code: 'BadGatewayError', message: 'Target could not be read', error: err })) + return + } + + res.writeHead(500, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ code: 'InternalServerError', message: 'Internal server error', error: err })) + }) +} + +const handleUpgrade = (server: SimpleProxyServer) => (req: IncomingMessage, socket: Socket, head: Buffer) => { + const { proxies } = server + const url = req.url as string // see IncomingMessage + + const proxy = proxies.find(p => url.startsWith(p.pathPrefix)) + if (!proxy) { + // TODO hpello notify socket? + socket.destroy() + return + } + + const { pathPrefix } = proxy + if (pathPrefix !== '/') { + req.url = url.slice(pathPrefix.length) + } + + proxy.proxyServer.ws(req, socket, head) +} + +export class SimpleProxyServer { + server: Server + proxies: ProxyConfig[] + + constructor() { + this.proxies = [] + this.server = createHTTPServer(handleRequest(this)) + this.server.on('upgrade', handleUpgrade(this)) + } + + addTarget(pathPrefix: string, target: ProxyTarget) { + // @ts-ignore: we use the undocumented 'socketPath' parameter + const proxyServer = HTTPProxy.createProxyServer({ + target, + xfwd: true + }) + + this.proxies = this.proxies + .filter(p => p.pathPrefix !== pathPrefix) + .concat([{ pathPrefix, proxyServer }]) + .sort((a, b) => { // sort path prefix descending to ensure path processing order later + if (a.pathPrefix.length < b.pathPrefix.length) { return 1 } + if (a.pathPrefix.length > b.pathPrefix.length) { return -1 } + return 0 + }) + } + + removeTarget(pathPrefix: string) { + this.proxies = this.proxies.filter(p => p.pathPrefix !== pathPrefix) + } +} diff --git a/server/src/api/socket-notify.ts b/server/src/api/socket-notify.ts new file mode 100644 index 0000000..6096554 --- /dev/null +++ b/server/src/api/socket-notify.ts @@ -0,0 +1,34 @@ +import { socketTypes } from './constants' +import { State } from '../types/redux' +import { createLogger } from '../utils/log' + +const log = createLogger(__filename) + +export class SocketNotify { + state: State | null + sockets: Set = new Set() + + constructor(state: State) { + this.state = state + } + + register(socket: SocketIO.Socket) { + if (!this.sockets.has(socket)) { log.debug('register socket %s', socket.id) } + this.sockets.add(socket) + if (this.state !== null) { + socket.emit(socketTypes.state, this.state) + } + } + + unregister(socket: SocketIO.Socket) { + if (this.sockets.has(socket)) { log.debug('unregister socket %s', socket.id) } + this.sockets.delete(socket) + } + + onStateChange(state: State) { + this.state = state + this.sockets.forEach((socket) => { + socket.emit(socketTypes.state, this.state) + }) + } +} diff --git a/server/src/api/socket-server.ts b/server/src/api/socket-server.ts new file mode 100644 index 0000000..315e3ba --- /dev/null +++ b/server/src/api/socket-server.ts @@ -0,0 +1,36 @@ +import * as SocketIOServer from 'socket.io' + +import { SOCKET_PATH, socketTypes } from './constants' +import { setupSocket } from './socket-setup' +import { SocketNotify } from './socket-notify' +import { Store } from '../types/redux' +import { createLogger } from '../utils/log' + +const log = createLogger(__filename) + +export const createIO = (store: Store, socketNotify: SocketNotify) => { + const io = SocketIOServer({ + path: SOCKET_PATH, + serveClient: false + }) + + io.on('connection', (socket) => { + const xfwdfor = socket.handshake.headers['x-forwarded-for'] + const address = xfwdfor + ? xfwdfor.split(',')[0] + : socket.handshake.address + log.info('client connected: %s at %s', socket.id, address) + + socket.on(socketTypes.register, () => socketNotify.register(socket)) + socket.on(socketTypes.unregister, () => socketNotify.unregister(socket)) + + socket.on('disconnect', () => { + socketNotify.unregister(socket) + log.info('client disconnected: %s at %s', socket.id, address) + }) + + setupSocket(socket, store) + }) + + return io +} diff --git a/server/src/api/socket-setup.ts b/server/src/api/socket-setup.ts new file mode 100644 index 0000000..ca5efe1 --- /dev/null +++ b/server/src/api/socket-setup.ts @@ -0,0 +1,71 @@ +import { thunks as autoconnectThunks } from '../autoconnect' +import { thunks as autoforwardThunks } from '../autoforward' +import { thunks as hostThunks } from '../host' +import { thunks as forwardThunks } from '../forward' +import { APIEndpoint, apiKeys } from './api' +import { socketTypes, SocketMessageError } from './constants' +import { AsyncThunkAction, Store } from '../types/redux' +import { createLogger } from '../utils/log' + +const log = createLogger(__filename) + +const CONNECT_REASON = 'api' +const FORWARD_REASON = 'api' + +const makeActions = (e: APIEndpoint): AsyncThunkAction[] | null => { + switch (e.key) { + case apiKeys.hostCreate: return [ + hostThunks.hostCreate(e.args.id, e.args.config), + autoconnectThunks.autoconnectCreate(e.args.id, e.args.autoConfig) + ] + case apiKeys.hostEdit: return [ + hostThunks.hostEdit(e.args.id, e.args.config), + autoconnectThunks.autoconnectEdit(e.args.id, e.args.autoConfig) + ] + case apiKeys.hostDelete: return [ + hostThunks.hostDelete(e.args.id), + autoconnectThunks.autoconnectDelete(e.args.id) + ] + case apiKeys.hostConnect: return [hostThunks.hostConnect(e.args.id, CONNECT_REASON)] + case apiKeys.hostDisconnect: return [hostThunks.hostDisconnect(e.args.id, CONNECT_REASON)] + + case apiKeys.forwardingCreate: return [ + forwardThunks.forwardingCreate(e.args.id, e.args.fwdId, e.args.config), + autoforwardThunks.autoforwardCreate(e.args.id, e.args.fwdId, e.args.autoConfig) + ] + case apiKeys.forwardingEdit: return [ + forwardThunks.forwardingEdit(e.args.id, e.args.fwdId, e.args.config), + autoforwardThunks.autoforwardEdit(e.args.id, e.args.fwdId, e.args.autoConfig) + ] + case apiKeys.forwardingDelete: return [ + forwardThunks.forwardingDelete(e.args.id, e.args.fwdId), + autoforwardThunks.autoforwardDelete(e.args.id, e.args.fwdId) + ] + case apiKeys.forwardingConnect: return [forwardThunks.forwardingConnect(e.args.id, e.args.fwdId, FORWARD_REASON)] + case apiKeys.forwardingDisconnect: return [forwardThunks.forwardingDisconnect(e.args.id, e.args.fwdId, FORWARD_REASON)] + default: return null + } +} + +export const setupSocket = (socket: SocketIO.Socket, store: Store) => { + socket.on(socketTypes.apiCall, (e: APIEndpoint, callback: (err: SocketMessageError | null, result: any) => any) => { + const actions = makeActions(e) + + if (actions === null) { + const message = `unhandled api call key: ${e.key}` + log.error({ err: new Error(message) }) + callback({ message }, null) + return + } + + actions.reduce((acc, val) => acc.then(() => store.dispatch(val)), Promise.resolve()) + .then((result) => { + log.info({ apiCall: e }, 'api call success') + callback(null, result) + }) + .catch((err: Error) => { + log.error({ err, apiCall: e }, 'api call failure') + callback({ message: err.message }, null) + }) + }) +} diff --git a/server/src/api/static-server.ts b/server/src/api/static-server.ts new file mode 100644 index 0000000..1a4cb41 --- /dev/null +++ b/server/src/api/static-server.ts @@ -0,0 +1,20 @@ +import { join } from 'path' +import { createServer as restifyCreateServer, plugins } from 'restify' + +export const createServer = () => { + const server = restifyCreateServer() + + server.use(plugins.gzipResponse()) + + server.get(/\/fonts\/.*/, plugins.serveStatic({ + directory: join(__dirname, '../../../gui/node_modules/font-awesome/fonts'), + appendRequestPath: false + })) + + server.get(/\/.*/, plugins.serveStatic({ + directory: join(__dirname, '../../../gui/public'), + default: 'index.html' + })) + + return server +} diff --git a/server/src/api/utils.ts b/server/src/api/utils.ts new file mode 100644 index 0000000..8ec07f8 --- /dev/null +++ b/server/src/api/utils.ts @@ -0,0 +1,55 @@ +import { find } from 'lodash' + +import { ForwardingStatus, fwdTypes } from '../forward' +import { ProxyTarget } from './simple-proxy-server' +import { State } from '../types/redux' + +const getForwardingStatus = (state: State, id: string, fwdId: string): ForwardingStatus | null => { + const forwarding = find(state.forwardings, { id, fwdId }) + return !forwarding ? null : forwarding.state.status +} + +const getForwardingBind = (state: State, id: string, fwdId: string): string | null => { + const forwarding = find(state.forwardings, { id, fwdId }) + if (!forwarding) { return null } + if (!forwarding.state.params) { return null } + return forwarding.state.params.bind +} + +type AddForwardingProxy = (id: string, fwdId: string, target: ProxyTarget) => void +type RemoveForwardingProxy = (id: string, fwdId: string) => void + +const makeProxyTarget = (address: string): ProxyTarget | null => { + if (address.includes('/')) { return { socketPath: address } } + if ((address.match(/:/g) || []).length !== 1) { return null } + const [host, portStr] = address.split(':') + const port = parseInt(portStr, 10) + return { host, port } +} + +const addForwardingProxies = (prevState: State, state: State, addForwardingProxy: AddForwardingProxy) => { + state.forwardings + .filter(({ config }) => config.spec.type === fwdTypes.http) + .filter(({ id, fwdId }) => getForwardingStatus(prevState, id, fwdId) !== 'connected') + .filter(({ id, fwdId }) => getForwardingStatus(state, id, fwdId) === 'connected') + .forEach(({ id, fwdId }) => { + const bind = getForwardingBind(state, id, fwdId) + if (bind === null) { return } + const target = makeProxyTarget(bind) + if (target === null) { return } + addForwardingProxy(id, fwdId, target) + }) +} + +const removeForwardingProxies = (prevState: State, state: State, removeForwardingProxy: RemoveForwardingProxy) => { + state.forwardings + .filter(({ config }) => config.spec.type === fwdTypes.http) + .filter(({ id, fwdId }) => getForwardingStatus(prevState, id, fwdId) === 'connected') + .filter(({ id, fwdId }) => getForwardingStatus(state, id, fwdId) !== 'connected') + .forEach(({ id, fwdId }) => { removeForwardingProxy(id, fwdId) }) +} + +export const onStateChange = (prevState: State, state: State, addForwardingProxy: AddForwardingProxy, removeForwardingProxy: RemoveForwardingProxy) => { + addForwardingProxies(prevState, state, addForwardingProxy) + removeForwardingProxies(prevState, state, removeForwardingProxy) +} diff --git a/server/src/autoconnect/actions.ts b/server/src/autoconnect/actions.ts new file mode 100644 index 0000000..e1b9bf5 --- /dev/null +++ b/server/src/autoconnect/actions.ts @@ -0,0 +1,48 @@ +import { AutoconnectConfig } from './types' + +export enum types { + AUTOCONNECT_CREATE = 'AUTOCONNECT_CREATE', + AUTOCONNECT_EDIT = 'AUTOCONNECT_EDIT', + AUTOCONNECT_DELETE = 'AUTOCONNECT_DELETE', + AUTOCONNECT_LAUNCH = 'AUTOCONNECT_LAUNCH', + AUTOCONNECT_CANCEL = 'AUTOCONNECT_CANCEL' +} + +interface AutoconnectCreateAction { + type: types.AUTOCONNECT_CREATE, id: string, config: AutoconnectConfig +} + +interface AutoconnectEditAction { + type: types.AUTOCONNECT_EDIT, id: string, config: AutoconnectConfig +} + +interface AutoconnectDeleteAction { + type: types.AUTOCONNECT_DELETE, id: string +} + +interface AutoconnectLaunchAction { + type: types.AUTOCONNECT_LAUNCH, + id: string, + autoretryId: string, + numRetries: number, + timeout: number +} + +interface AutoconnectCancelAction { + type: types.AUTOCONNECT_CANCEL, id: string +} + +export type Action = + | AutoconnectCreateAction + | AutoconnectEditAction + | AutoconnectDeleteAction + | AutoconnectLaunchAction + | AutoconnectCancelAction + +export const actions = { + autoconnectCreate: (id: string, config: AutoconnectConfig): Action => ({ type: types.AUTOCONNECT_CREATE, id, config }), + autoconnectEdit: (id: string, config: AutoconnectConfig): Action => ({ type: types.AUTOCONNECT_EDIT, id, config }), + autoconnectDelete: (id: string): Action => ({ type: types.AUTOCONNECT_DELETE, id }), + autoconnectLaunch: (id: string, autoretryId: string, numRetries: number, timeout: number): Action => ({ type: types.AUTOCONNECT_LAUNCH, id, autoretryId, numRetries, timeout }), + autoconnectCancel: (id: string): Action => ({ type: types.AUTOCONNECT_CANCEL, id }) +} diff --git a/server/src/autoconnect/component.ts b/server/src/autoconnect/component.ts new file mode 100644 index 0000000..f5f2b74 --- /dev/null +++ b/server/src/autoconnect/component.ts @@ -0,0 +1,23 @@ +import { State, Store } from '../types/redux' +import { onStateChange } from './utils' + +export class Autoconnector { + store: Store + prevState: State + + constructor(params: { store: Store }) { + this.store = params.store + this.prevState = params.store.getState() + } + + setup() { + this.store.subscribe(() => { + const state = this.store.getState() + + process.nextTick(() => { // prevent recursion + onStateChange(this.prevState, state, this.store.dispatch) + this.prevState = state + }) + }) + } +} diff --git a/server/src/autoconnect/constants.ts b/server/src/autoconnect/constants.ts new file mode 100644 index 0000000..2307fa2 --- /dev/null +++ b/server/src/autoconnect/constants.ts @@ -0,0 +1,2 @@ +export const CONNECT_REASON_AUTOSTART = 'autostart' +export const CONNECT_REASON_AUTORETRY = 'autoretry' diff --git a/server/src/autoconnect/index.ts b/server/src/autoconnect/index.ts new file mode 100644 index 0000000..113a21e --- /dev/null +++ b/server/src/autoconnect/index.ts @@ -0,0 +1,6 @@ +export * from './actions' +export * from './component' +export * from './constants' +export * from './reducer' +export * from './thunks' +export * from './types' diff --git a/server/src/autoconnect/reducer.ts b/server/src/autoconnect/reducer.ts new file mode 100644 index 0000000..f649cba --- /dev/null +++ b/server/src/autoconnect/reducer.ts @@ -0,0 +1,75 @@ +import { types } from './actions' +import { Action } from '../types/redux' +import { AutoconnectConfig } from './types' + +export interface AutoconnectSubState { + autoretryId: string | null, + numRetries: number, + timeout: number +} + +export interface AutoconnectState { + id: string, + config: AutoconnectConfig, + state: AutoconnectSubState +} + +export type State = AutoconnectState[] + +const initialState = (): State => ([]) + +const defaultSubState = (): AutoconnectSubState => ({ + autoretryId: null, + numRetries: 0, + timeout: 0 +}) + + +export const reducer = (state: State = initialState(), action: Action): State => { + switch (action.type) { + case types.AUTOCONNECT_CREATE: { + const { id, config } = action + + const autoconnectState = { + id, + config, + state: defaultSubState() + } + + return [...state, autoconnectState] + } + case types.AUTOCONNECT_EDIT: { + const { id, config } = action + return state.map(x => x.id === id ? ({ ...x, config }) : x) + } + case types.AUTOCONNECT_DELETE: { + const { id } = action + return state.filter(x => x.id !== id) + } + case types.AUTOCONNECT_LAUNCH: { + const { id, autoretryId, numRetries, timeout } = action + + return state.map((autoconnectState) => { + if (autoconnectState.id !== id) { return autoconnectState } + + return { + ...autoconnectState, + state: { autoretryId, numRetries, timeout } + } + }) + } + case types.AUTOCONNECT_CANCEL: { + const { id } = action + return state.map((autoconnectState) => { + if (autoconnectState.id !== id) { return autoconnectState } + + return { + ...autoconnectState, + state: defaultSubState() + } + }) + } + default: + return state + } +} diff --git a/server/src/autoconnect/thunks/create-thunks.ts b/server/src/autoconnect/thunks/create-thunks.ts new file mode 100644 index 0000000..9e03bcb --- /dev/null +++ b/server/src/autoconnect/thunks/create-thunks.ts @@ -0,0 +1,39 @@ +import { find } from 'lodash' + +import { actions } from '../actions' +import { AutoconnectConfig } from '../types' +import { AsyncThunkAction, Dispatch, GetState } from '../../types/redux' +import { ErrorWithCode } from '../../utils/error-with-code' +import { createLogger } from '../../utils/log' + +const log = createLogger(__filename) + +export const autoconnectCreate = (id: string, config: AutoconnectConfig): AsyncThunkAction => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const host = find(state.hosts, { id }) + if (!host) { log.info(`host not found: ${id}`); return } + + const autoconnect = find(state.autoconnects, { id }) + if (autoconnect) { throw new ErrorWithCode(409, `autoconnect already exists: ${id}`) } + + dispatch(actions.autoconnectCreate(id, config)) +} + +export const autoconnectEdit = (id: string, config: AutoconnectConfig): AsyncThunkAction => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const host = find(state.hosts, { id }) + if (!host) { log.info(`host not found: ${id}`); return } + + const autoconnect = find(state.autoconnects, { id }) + if (!autoconnect) { throw new ErrorWithCode(404, `autoconnect not found: ${id}`) } + + dispatch(actions.autoconnectEdit(id, config)) +} + +export const autoconnectDelete = (id: string): AsyncThunkAction => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const autoconnect = find(state.autoconnects, { id }) + if (!autoconnect) { log.info(`autoconnect not found: ${id}`); return } + + dispatch(actions.autoconnectDelete(id)) +} diff --git a/server/src/autoconnect/thunks/index.ts b/server/src/autoconnect/thunks/index.ts new file mode 100644 index 0000000..ecb42b1 --- /dev/null +++ b/server/src/autoconnect/thunks/index.ts @@ -0,0 +1,9 @@ +import { autoconnectCreate, autoconnectEdit, autoconnectDelete } from './create-thunks' +import { autoretrySpawn } from './retry-thunks' + +export const thunks = { + autoconnectCreate, + autoconnectEdit, + autoconnectDelete, + autoretrySpawn +} diff --git a/server/src/autoconnect/thunks/retry-thunks.ts b/server/src/autoconnect/thunks/retry-thunks.ts new file mode 100644 index 0000000..130a3e6 --- /dev/null +++ b/server/src/autoconnect/thunks/retry-thunks.ts @@ -0,0 +1,42 @@ +import { find } from 'lodash' + +import { actions } from '../actions' +import { thunks as hostThunks } from '../../host' +import { CONNECT_REASON_AUTORETRY } from '../constants' +import { AutoconnectState } from '../reducer' +import { Dispatch, GetState } from '../../types/redux' + +const MIN_TIMEOUT_MS = 100 +const MAX_TIMEOUT_MS = 5000 +const NEXT_TIMEOUT_FACTOR = 2 + +const makeNextTimeout = (autoconnect: AutoconnectState): number => { + if (autoconnect.state.timeout === 0) { return MIN_TIMEOUT_MS } + + const timeout = autoconnect.state.timeout * NEXT_TIMEOUT_FACTOR + if (timeout > MAX_TIMEOUT_MS) { return MAX_TIMEOUT_MS } + + return timeout +} + +const makeAutoretryId = (): string => process.hrtime().join('-') + +export const autoretrySpawn = (id: string) => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + + const autoconnect = find(state.autoconnects, { id }) + if (!autoconnect) { return } + + const autoretryId = makeAutoretryId() + const numRetries = autoconnect.state.numRetries + 1 + const timeout = makeNextTimeout(autoconnect) + + dispatch(actions.autoconnectLaunch(id, autoretryId, numRetries, timeout)) + + setTimeout(() => { + const a = find(getState().autoconnects, { id }) + if (a && a.state.autoretryId === autoretryId) { + dispatch(hostThunks.hostConnect(id, CONNECT_REASON_AUTORETRY)) + } + }, timeout) +} diff --git a/server/src/autoconnect/types.ts b/server/src/autoconnect/types.ts new file mode 100644 index 0000000..733a232 --- /dev/null +++ b/server/src/autoconnect/types.ts @@ -0,0 +1,4 @@ +export type AutoconnectConfig = { + start: boolean, + retry: boolean +} diff --git a/server/src/autoconnect/utils.ts b/server/src/autoconnect/utils.ts new file mode 100644 index 0000000..a2107be --- /dev/null +++ b/server/src/autoconnect/utils.ts @@ -0,0 +1,90 @@ +import { find, some } from 'lodash' + +import { actions } from './actions' +import { HostStatus, thunks as hostThunks } from '../host' +import { CONNECT_REASON_AUTOSTART, CONNECT_REASON_AUTORETRY } from './constants' +import { Dispatch, State } from '../types/redux' +import { thunks } from './thunks' + +const getStatus = (state: State, id: string): HostStatus | null => { + const host = find(state.hosts, { id }) + return !host ? null : host.state.status +} + +const getConnectReason = (state: State, id: string): string | null => { + const host = find(state.hosts, { id }) + return !host ? null : host.state.reason +} + +const getAutostart = (state: State, id: string): boolean | null => { + const autoconnect = find(state.autoconnects, { id }) + return !autoconnect ? null : autoconnect.config.start +} + +const getAutoretry = (state: State, id: string): boolean | null => { + const autoconnect = find(state.autoconnects, { id }) + return !autoconnect ? null : autoconnect.config.retry +} + +const autoconnectExist = (state: State, id: string): boolean => some(state.autoconnects, { id }) + +const hasActiveAutoretry = (state: State, id: string): boolean => { + const autoconnect = find(state.autoconnects, { id }) + return !autoconnect ? false : (autoconnect.state.autoretryId !== null) +} + +const includes = (array: T[], x: T | null): boolean => x === null ? false : array.includes(x) + +const autoStartHosts = (prevState: State, state: State, dispatch: Dispatch) => { + state.autoconnects.map(x => x.id) + .filter(id => !autoconnectExist(prevState, id)) + .filter(id => autoconnectExist(state, id)) + .filter(id => getAutostart(state, id)) + .forEach((id) => { + dispatch(hostThunks.hostConnect(id, CONNECT_REASON_AUTOSTART)) + }) +} + +const autoRetryHosts = (prevState: State, state: State, dispatch: Dispatch) => { + state.autoconnects.map(x => x.id) + .filter(id => getAutoretry(state, id)) + .filter(id => !includes(['error'], getStatus(prevState, id))) + .filter(id => includes(['error'], getStatus(state, id))) + .forEach((id) => { + dispatch(thunks.autoretrySpawn(id)) + }) +} + +const cancelAutoconnects = (prevState: State, state: State, dispatch: Dispatch) => { + state.autoconnects.map(x => x.id) + .filter(id => getStatus(prevState, id) !== 'connected') + .filter(id => getStatus(state, id) === 'connected') + .filter(id => hasActiveAutoretry(state, id)) + .forEach((id) => { + dispatch(actions.autoconnectCancel(id)) + }) + + state.autoconnects.map(x => x.id) + .filter(id => getStatus(prevState, id) !== 'connecting') + .filter(id => getStatus(state, id) === 'connecting') + .filter(id => getConnectReason(state, id) !== CONNECT_REASON_AUTOSTART && getConnectReason(state, id) !== CONNECT_REASON_AUTORETRY) + .filter(id => hasActiveAutoretry(state, id)) + .forEach((id) => { + dispatch(actions.autoconnectCancel(id)) + }) + + state.autoconnects.map(x => x.id) + .filter(id => getStatus(prevState, id) !== 'disconnecting') + .filter(id => getStatus(state, id) === 'disconnecting') + .filter(id => getConnectReason(state, id) !== CONNECT_REASON_AUTOSTART && getConnectReason(state, id) !== CONNECT_REASON_AUTORETRY) + .filter(id => hasActiveAutoretry(state, id)) + .forEach((id) => { + dispatch(actions.autoconnectCancel(id)) + }) +} + +export const onStateChange = (prevState: State, state: State, dispatch: Dispatch) => { + autoStartHosts(prevState, state, dispatch) + autoRetryHosts(prevState, state, dispatch) + cancelAutoconnects(prevState, state, dispatch) +} diff --git a/server/src/autoforward/actions.ts b/server/src/autoforward/actions.ts new file mode 100644 index 0000000..b5eb4e7 --- /dev/null +++ b/server/src/autoforward/actions.ts @@ -0,0 +1,49 @@ +import { AutoforwardConfig } from './types' + +export enum types { + AUTOFORWARD_CREATE = 'AUTOFORWARD_CREATE', + AUTOFORWARD_EDIT = 'AUTOFORWARD_EDIT', + AUTOFORWARD_DELETE = 'AUTOFORWARD_DELETE', + AUTOFORWARD_LAUNCH = 'AUTOFORWARD_LAUNCH', + AUTOFORWARD_CANCEL = 'AUTOFORWARD_CANCEL' +} + +interface AutoforwardCreateAction { + type: types.AUTOFORWARD_CREATE, id: string, fwdId: string, config: AutoforwardConfig +} + +interface AutoforwardEditAction { + type: types.AUTOFORWARD_EDIT, id: string, fwdId: string, config: AutoforwardConfig +} + +interface AutoforwardDeleteAction { + type: types.AUTOFORWARD_DELETE, id: string, fwdId: string +} + +interface AutoforwardLaunchAction { + type: types.AUTOFORWARD_LAUNCH, + id: string, + fwdId: string, + autoretryId: string, + numRetries: number, + timeout: number +} + +interface AutoforwardCancelAction { + type: types.AUTOFORWARD_CANCEL, id: string, fwdId: string +} + +export type Action = + | AutoforwardCreateAction + | AutoforwardEditAction + | AutoforwardDeleteAction + | AutoforwardLaunchAction + | AutoforwardCancelAction + +export const actions = { + autoforwardCreate: (id: string, fwdId: string, config: AutoforwardConfig): Action => ({ type: types.AUTOFORWARD_CREATE, id, fwdId, config }), + autoforwardEdit: (id: string, fwdId: string, config: AutoforwardConfig): Action => ({ type: types.AUTOFORWARD_EDIT, id, fwdId, config }), + autoforwardDelete: (id: string, fwdId: string): Action => ({ type: types.AUTOFORWARD_DELETE, id, fwdId }), + autoforwardLaunch: (id: string, fwdId: string, autoretryId: string, numRetries: number, timeout: number): Action => ({ type: types.AUTOFORWARD_LAUNCH, id, fwdId, autoretryId, numRetries, timeout }), + autoforwardCancel: (id: string, fwdId: string): Action => ({ type: types.AUTOFORWARD_CANCEL, id, fwdId }) +} diff --git a/server/src/autoforward/component.ts b/server/src/autoforward/component.ts new file mode 100644 index 0000000..2853f6a --- /dev/null +++ b/server/src/autoforward/component.ts @@ -0,0 +1,23 @@ +import { State, Store } from '../types/redux' +import { onStateChange } from './utils' + +export class Autoforwarder { + store: Store + prevState: State + + constructor(params: { store: Store }) { + this.store = params.store + this.prevState = params.store.getState() + } + + setup() { + this.store.subscribe(() => { + const state = this.store.getState() + + process.nextTick(() => { // prevent recursion + onStateChange(this.prevState, state, this.store.dispatch) + this.prevState = state + }) + }) + } +} diff --git a/server/src/autoforward/constants.ts b/server/src/autoforward/constants.ts new file mode 100644 index 0000000..41c5697 --- /dev/null +++ b/server/src/autoforward/constants.ts @@ -0,0 +1,2 @@ +export const FORWARD_REASON_AUTOSTART = 'autostart' +export const FORWARD_REASON_AUTORETRY = 'autoretry' diff --git a/server/src/autoforward/index.ts b/server/src/autoforward/index.ts new file mode 100644 index 0000000..113a21e --- /dev/null +++ b/server/src/autoforward/index.ts @@ -0,0 +1,6 @@ +export * from './actions' +export * from './component' +export * from './constants' +export * from './reducer' +export * from './thunks' +export * from './types' diff --git a/server/src/autoforward/reducer.ts b/server/src/autoforward/reducer.ts new file mode 100644 index 0000000..2a42cd3 --- /dev/null +++ b/server/src/autoforward/reducer.ts @@ -0,0 +1,77 @@ +import { types } from './actions' +import { Action } from '../types/redux' +import { AutoforwardConfig } from './types' + +export interface AutoforwardSubState { + autoretryId: string | null, + numRetries: number, + timeout: number +} + +export interface AutoforwardState { + id: string, + fwdId: string, + config: AutoforwardConfig, + state: AutoforwardSubState +} + +export type State = AutoforwardState[] + +const initialState = (): State => ([]) + +const defaultSubState = (): AutoforwardSubState => ({ + autoretryId: null, + numRetries: 0, + timeout: 0 +}) + + +export const reducer = (state: State = initialState(), action: Action): State => { + switch (action.type) { + case types.AUTOFORWARD_CREATE: { + const { id, fwdId, config } = action + + const autoforwardState = { + id, + fwdId, + config, + state: defaultSubState() + } + + return [...state, autoforwardState] + } + case types.AUTOFORWARD_EDIT: { + const { id, fwdId, config } = action + return state.map(x => x.id === id && x.fwdId === fwdId ? ({ ...x, config }) : x) + } + case types.AUTOFORWARD_DELETE: { + const { id, fwdId } = action + return state.filter(x => x.id !== id || x.fwdId !== fwdId) + } + case types.AUTOFORWARD_LAUNCH: { + const { id, fwdId, autoretryId, numRetries, timeout } = action + + return state.map((autoforwardState) => { + if (autoforwardState.id !== id || autoforwardState.fwdId !== fwdId) { return autoforwardState } + + return { + ...autoforwardState, + state: { autoretryId, numRetries, timeout } + } + }) + } + case types.AUTOFORWARD_CANCEL: { + const { id, fwdId } = action + return state.map((autoforwardState) => { + if (autoforwardState.id !== id || autoforwardState.fwdId !== fwdId) { return autoforwardState } + + return { + ...autoforwardState, + state: defaultSubState() + } + }) + } + default: + return state + } +} diff --git a/server/src/autoforward/thunks/create-thunks.ts b/server/src/autoforward/thunks/create-thunks.ts new file mode 100644 index 0000000..d339f96 --- /dev/null +++ b/server/src/autoforward/thunks/create-thunks.ts @@ -0,0 +1,39 @@ +import { find } from 'lodash' + +import { actions } from '../actions' +import { AutoforwardConfig } from '../types' +import { AsyncThunkAction, Dispatch, GetState } from '../../types/redux' +import { ErrorWithCode } from '../../utils/error-with-code' +import { createLogger } from '../../utils/log' + +const log = createLogger(__filename) + +export const autoforwardCreate = (id: string, fwdId: string, config: AutoforwardConfig): AsyncThunkAction => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const host = find(state.hosts, { id }) + if (!host) { log.info(`host not found: ${id}`); return } + + const autoforward = find(state.autoforwards, { id, fwdId }) + if (autoforward) { throw new ErrorWithCode(409, `autoforward already exists: ${id}/${fwdId}`) } + + dispatch(actions.autoforwardCreate(id, fwdId, config)) +} + +export const autoforwardEdit = (id: string, fwdId: string, config: AutoforwardConfig): AsyncThunkAction => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const host = find(state.hosts, { id }) + if (!host) { log.info(`host not found: ${id}`); return } + + const autoforward = find(state.autoforwards, { id, fwdId }) + if (!autoforward) { throw new ErrorWithCode(404, `autoforward not found: ${id}/${fwdId}`) } + + dispatch(actions.autoforwardEdit(id, fwdId, config)) +} + +export const autoforwardDelete = (id: string, fwdId: string): AsyncThunkAction => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const autoforward = find(state.autoforwards, { id, fwdId }) + if (!autoforward) { log.info(`autoforward not found: ${id}/${fwdId}`); return } + + dispatch(actions.autoforwardDelete(id, fwdId)) +} diff --git a/server/src/autoforward/thunks/index.ts b/server/src/autoforward/thunks/index.ts new file mode 100644 index 0000000..753e689 --- /dev/null +++ b/server/src/autoforward/thunks/index.ts @@ -0,0 +1,9 @@ +import { autoforwardCreate, autoforwardEdit, autoforwardDelete } from './create-thunks' +import { autoretrySpawn } from './retry-thunks' + +export const thunks = { + autoforwardCreate, + autoforwardEdit, + autoforwardDelete, + autoretrySpawn +} diff --git a/server/src/autoforward/thunks/retry-thunks.ts b/server/src/autoforward/thunks/retry-thunks.ts new file mode 100644 index 0000000..f8f4640 --- /dev/null +++ b/server/src/autoforward/thunks/retry-thunks.ts @@ -0,0 +1,42 @@ +import { find } from 'lodash' + +import { actions } from '../actions' +import { thunks as forwardTunks } from '../../forward' +import { FORWARD_REASON_AUTORETRY } from '../constants' +import { AutoforwardState } from '../reducer' +import { Dispatch, GetState } from '../../types/redux' + +const MIN_TIMEOUT_MS = 100 +const MAX_TIMEOUT_MS = 5000 +const NEXT_TIMEOUT_FACTOR = 2 + +const makeNextTimeout = (autoforward: AutoforwardState): number => { + if (autoforward.state.timeout === 0) { return MIN_TIMEOUT_MS } + + const timeout = autoforward.state.timeout * NEXT_TIMEOUT_FACTOR + if (timeout > MAX_TIMEOUT_MS) { return MAX_TIMEOUT_MS } + + return timeout +} + +const makeAutoretryId = (): string => process.hrtime().join('-') + +export const autoretrySpawn = (id: string, fwdId: string) => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + + const autoforward = find(state.autoforwards, { id, fwdId }) + if (!autoforward) { return } + + const autoretryId = makeAutoretryId() + const numRetries = autoforward.state.numRetries + 1 + const timeout = makeNextTimeout(autoforward) + + dispatch(actions.autoforwardLaunch(id, fwdId, autoretryId, numRetries, timeout)) + + setTimeout(() => { + const a = find(getState().autoforwards, { id, fwdId }) + if (a && a.state.autoretryId === autoretryId) { + dispatch(forwardTunks.forwardingConnect(id, fwdId, FORWARD_REASON_AUTORETRY)) + } + }, timeout) +} diff --git a/server/src/autoforward/types.ts b/server/src/autoforward/types.ts new file mode 100644 index 0000000..231c80c --- /dev/null +++ b/server/src/autoforward/types.ts @@ -0,0 +1,4 @@ +export type AutoforwardConfig = { + start: boolean, + retry: boolean +} diff --git a/server/src/autoforward/utils.ts b/server/src/autoforward/utils.ts new file mode 100644 index 0000000..0390fea --- /dev/null +++ b/server/src/autoforward/utils.ts @@ -0,0 +1,94 @@ +import { find } from 'lodash' + +import { actions } from './actions' +import { HostStatus } from '../host' +import { ForwardingStatus, thunks as forwardThunks } from '../forward' +import { FORWARD_REASON_AUTOSTART, FORWARD_REASON_AUTORETRY } from './constants' +import { Dispatch, State } from '../types/redux' +import { thunks } from './thunks' + +const getHostStatus = (state: State, id: string): HostStatus | null => { + const host = find(state.hosts, { id }) + return !host ? null : host.state.status +} + +const getForwardingStatus = (state: State, id: string, fwdId: string): ForwardingStatus | null => { + const forwarding = find(state.forwardings, { id, fwdId }) + return !forwarding ? null : forwarding.state.status +} + +const getForwardReason = (state: State, id: string, fwdId: string): string | null => { + const forwarding = find(state.forwardings, { id, fwdId }) + return !forwarding ? null : forwarding.state.reason +} + +const getAutostart = (state: State, id: string, fwdId: string): boolean | null => { + const autoforward = find(state.autoforwards, { id, fwdId }) + return !autoforward ? null : autoforward.config.start +} + +const getAutoretry = (state: State, id: string, fwdId: string): boolean | null => { + const autoforward = find(state.autoforwards, { id, fwdId }) + return !autoforward ? null : autoforward.config.retry +} + +const hasActiveAutoretry = (state: State, id: string, fwdId: string): boolean => { + const autoforward = find(state.autoforwards, { id, fwdId }) + return !autoforward ? false : (autoforward.state.autoretryId !== null) +} + +const includes = (array: T[], x: T | null): boolean => x === null ? false : array.includes(x) + +const autoStartForwardings = (prevState: State, state: State, dispatch: Dispatch) => { + state.autoforwards.map(x => ({ id: x.id, fwdId: x.fwdId })) + .filter(({ id }) => getHostStatus(prevState, id) !== 'connected') + .filter(({ id }) => getHostStatus(state, id) === 'connected') + .filter(({ id, fwdId }) => getAutostart(state, id, fwdId)) + .forEach(({ id, fwdId }) => { + dispatch(forwardThunks.forwardingConnect(id, fwdId, FORWARD_REASON_AUTOSTART)) + }) +} + +const autoRetryForwardings = (prevState: State, state: State, dispatch: Dispatch) => { + state.autoforwards.map(x => ({ id: x.id, fwdId: x.fwdId })) + .filter(({ id, fwdId }) => getAutoretry(state, id, fwdId)) + .filter(({ id, fwdId }) => !includes(['error'], getForwardingStatus(prevState, id, fwdId))) + .filter(({ id, fwdId }) => includes(['error'], getForwardingStatus(state, id, fwdId))) + .forEach(({ id, fwdId }) => { + dispatch(thunks.autoretrySpawn(id, fwdId)) + }) +} + +const cancelAutoforwards = (prevState: State, state: State, dispatch: Dispatch) => { + state.autoforwards.map(x => ({ id: x.id, fwdId: x.fwdId })) + .filter(({ id, fwdId }) => getForwardingStatus(prevState, id, fwdId) !== 'connected') + .filter(({ id, fwdId }) => getForwardingStatus(state, id, fwdId) === 'connected') + .filter(({ id, fwdId }) => hasActiveAutoretry(state, id, fwdId)) + .forEach(({ id, fwdId }) => { + dispatch(actions.autoforwardCancel(id, fwdId)) + }) + + state.autoforwards.map(x => ({ id: x.id, fwdId: x.fwdId })) + .filter(({ id, fwdId }) => getForwardingStatus(prevState, id, fwdId) !== 'connecting') + .filter(({ id, fwdId }) => getForwardingStatus(state, id, fwdId) === 'connecting') + .filter(({ id, fwdId }) => getForwardReason(state, id, fwdId) !== FORWARD_REASON_AUTOSTART && getForwardReason(state, id, fwdId) !== FORWARD_REASON_AUTORETRY) + .filter(({ id, fwdId }) => hasActiveAutoretry(state, id, fwdId)) + .forEach(({ id, fwdId }) => { + dispatch(actions.autoforwardCancel(id, fwdId)) + }) + + state.autoforwards.map(x => ({ id: x.id, fwdId: x.fwdId })) + .filter(({ id, fwdId }) => getForwardingStatus(prevState, id, fwdId) !== 'disconnecting') + .filter(({ id, fwdId }) => getForwardingStatus(state, id, fwdId) === 'disconnecting') + .filter(({ id, fwdId }) => getForwardReason(state, id, fwdId) !== FORWARD_REASON_AUTOSTART && getForwardReason(state, id, fwdId) !== FORWARD_REASON_AUTORETRY) + .filter(({ id, fwdId }) => hasActiveAutoretry(state, id, fwdId)) + .forEach(({ id, fwdId }) => { + dispatch(actions.autoforwardCancel(id, fwdId)) + }) +} + +export const onStateChange = (prevState: State, state: State, dispatch: Dispatch) => { + autoStartForwardings(prevState, state, dispatch) + autoRetryForwardings(prevState, state, dispatch) + cancelAutoforwards(prevState, state, dispatch) +} diff --git a/server/src/cli.ts b/server/src/cli.ts new file mode 100644 index 0000000..b2dc0b4 --- /dev/null +++ b/server/src/cli.ts @@ -0,0 +1,17 @@ +import * as yargs from 'yargs' +import { start, EngineOptions } from './engine' + +const argv = yargs + .usage('Usage: $0 [options]') + .options({ + c: { + alias: 'config-file', + describe: 'Path to sshmon config file', + type: 'string' + } + }) + .help().alias('h', 'help') + .version().alias('v', 'version') + .parse(process.argv) + +start(argv as EngineOptions) diff --git a/server/src/config/actions.ts b/server/src/config/actions.ts new file mode 100644 index 0000000..6df2247 --- /dev/null +++ b/server/src/config/actions.ts @@ -0,0 +1,16 @@ +import { ConfigConfig } from './types' + +export enum types { + CONFIG_EDIT = 'CONFIG_EDIT' +} + +interface ConfigEditAction { + type: types.CONFIG_EDIT, config: ConfigConfig +} + +export type Action = + | ConfigEditAction + +export const actions = { + configEdit: (config: ConfigConfig): Action => ({ type: types.CONFIG_EDIT, config }) +} diff --git a/server/src/config/component.ts b/server/src/config/component.ts new file mode 100644 index 0000000..6f9c0fc --- /dev/null +++ b/server/src/config/component.ts @@ -0,0 +1,128 @@ +import * as stringify from 'json-stable-stringify' + +import { access, mkdir, stat, writeFile } from 'fs' +import { homedir } from 'os' +import { join } from 'path' +import { promisify } from 'util' +const accessAsync = promisify(access) +const mkdirAsync = promisify(mkdir) +const statAsync = promisify(stat) +const writeFileAsync = promisify(writeFile) + +import { HostConfig, thunks as hostThunks } from '../host' +import { ForwardingConfig, thunks as forwardThunks } from '../forward' +import { AutoconnectConfig, thunks as autoconnectThunks } from '../autoconnect' +import { AutoforwardConfig, thunks as autoforwardThunks } from '../autoforward' +import { State, Store } from '../types/redux' + +import { actions as configActions } from './actions' +import { configObjectToState, configStateToObject } from './convert' +import { ConfigSchema } from './schema' +import { load, save } from './serialize' +import { ConfigConfig, ConfigType } from './types' +import { createLogger } from '../utils/log' + +const log = createLogger(__filename) + +const dispatchConfigActions = async (config: ConfigType, store: Store) => { + config.hosts.forEach(({ id, config }) => { + store.dispatch(hostThunks.hostCreate(id, config)) + }) + config.forwardings.forEach(({ id, fwdId, config }) => { + store.dispatch(forwardThunks.forwardingCreate(id, fwdId, config)) + }) + config.autoconnects.forEach(({ id, config }) => { + store.dispatch(autoconnectThunks.autoconnectCreate(id, config)) + }) + config.autoforwards.forEach(({ id, fwdId, config }) => { + store.dispatch(autoforwardThunks.autoforwardCreate(id, fwdId, config)) + }) + store.dispatch(configActions.configEdit(config.config)) +} + +const extractConfigFromState = (state: State): ConfigType => { + const hosts: { id: string, config: HostConfig }[] = state.hosts.map(({ id, config }) => ({ id, config })) + const forwardings: { id: string, fwdId: string, config: ForwardingConfig }[] = state.forwardings.map(({ id, fwdId, config }) => ({ id, fwdId, config })) + const autoconnects: { id: string, config: AutoconnectConfig }[] = state.autoconnects.map(({ id, config }) => ({ id, config })) + const autoforwards: { id: string, fwdId: string, config: AutoforwardConfig }[] = state.autoforwards.map(({ id, fwdId, config }) => ({ id, fwdId, config })) + const config: ConfigConfig = state.config + return { hosts, forwardings, autoconnects, autoforwards, config } +} + +const saveConfigIfNeeded = async (state: State, prevState: State, path: string) => { + const config = extractConfigFromState(state) + const prevConfig = extractConfigFromState(prevState) + if (stringify(config) === stringify(prevConfig)) { return } + + const configObject = configStateToObject(config) + if (config.config.autosave) { + save(configObject, path) + } +} + +const exist = async (path: string): Promise => { + try { + await accessAsync(path) + } catch { + return false + } + return true +} + +const defaultConfig = (): ConfigSchema => ({ + config: { + autosave: true + } +}) + +const ensureDefaultConfigFile = async (): Promise => { + const dir = join(homedir(), '.sshmon') + if (!(await exist(dir))) { + log.debug('create directory at', dir) + await mkdirAsync(dir, 0o700) + } + const statDir = await statAsync(dir) + if ((statDir.mode & 0o777) !== 0o700) { throw new Error(`Bad permissions for config dir ${dir}`) } + + const configFile = join(dir, 'config.yml') + if (!(await exist(configFile))) { + log.debug('create config file at', configFile) + await writeFileAsync(configFile, '', { mode: 0o644 }) + await save(defaultConfig(), configFile) + } + const statFile = await statAsync(configFile) + if ((statFile.mode & 0o777) !== 0o644) { throw new Error(`Bad permissions for config file ${configFile}`) } + + return configFile +} + +export class Config { + store: Store + prevState: State + configPath: string + + constructor(params: { store: Store, configPath: string | null }) { + const { store, configPath } = params + this.store = store + this.prevState = params.store.getState() + this.configPath = configPath === null ? '' : configPath + } + + async setup() { + if (this.configPath === '') { + this.configPath = await ensureDefaultConfigFile() + } + + log.info('config file is', this.configPath) + const config = await load(this.configPath) + const configState = configObjectToState(config) + log.debug({ config: JSON.stringify(configState) }, 'loaded config') + + await dispatchConfigActions(configState, this.store) + this.store.subscribe(() => { + const state = this.store.getState() + saveConfigIfNeeded(state, this.prevState, this.configPath) + this.prevState = state + }) + } +} diff --git a/server/src/config/convert.ts b/server/src/config/convert.ts new file mode 100644 index 0000000..dd37b72 --- /dev/null +++ b/server/src/config/convert.ts @@ -0,0 +1,144 @@ +import { find, flatten, map } from 'lodash' +import { ConfigConfig, ConfigType } from './types' +import { parseForwardingSpec, serializeForwardingSpec } from './forwarding-spec' +import { ConfigConfigSchema, ConfigSchema, HostSchema, ForwardingSchema } from './schema' +import { HostConfig } from '../host' +import { ForwardingConfig } from '../forward' +import { AutoconnectConfig } from '../autoconnect' +import { AutoforwardConfig } from '../autoforward' + +// object to state +const makeForwardingState = (forwarding: ForwardingSchema): ForwardingConfig => { + if (typeof forwarding === 'string') { + return { spec: parseForwardingSpec(forwarding), label: '' } + } + + const spec = parseForwardingSpec(forwarding.spec) + const label = forwarding.label || '' + + return { spec, label } +} + +const makeHostState = (id: string, host: HostSchema): HostConfig => { + const sshHost = host.ssh && host.ssh.host ? host.ssh.host : id + const sshConfig = host.ssh && host.ssh.config ? host.ssh.config : {} + const ssh = { + host: sshHost, + config: sshConfig + } + const label = host.label || '' + + return { ssh, label } +} + +const makeAutoconnectState = (host: HostSchema): AutoconnectConfig => { + const start = host.autostart || false + const retry = host.autoretry || false + return { start, retry } +} + +const makeAutoforwardState = (forwarding: ForwardingSchema): AutoforwardConfig => { + if (typeof forwarding === 'string') { + return { start: false, retry: false } + } + + const start = forwarding.autostart || false + const retry = forwarding.autoretry || false + return { start, retry } +} + +const makeConfigState = (config: ConfigConfigSchema): ConfigConfig => ({ + autosave: config.autosave || false +}) + +export const configObjectToState = (config: ConfigSchema): ConfigType => { + const defaultConfigConfig = { autosave: false } + if (config === null) { + return { hosts: [], forwardings: [], autoconnects: [], autoforwards: [], config: defaultConfigConfig } + } + + const hosts = map(config.hosts || {}, (v, id) => ({ id, config: makeHostState(id, v) })) + const forwardings = flatten(map(config.hosts || {}, (v, id) => + map(v.forward || {}, (vv, fwdId) => ({ id, fwdId, config: makeForwardingState(vv) }) + ))) + const autoconnects = map(config.hosts || {}, (v, id) => ({ id, config: makeAutoconnectState(v) })) + const autoforwards = flatten(map(config.hosts || {}, (v, id) => + map(v.forward || {}, (vv, fwdId) => ({ id, fwdId, config: makeAutoforwardState(vv) }) + ))) + const configConfig = config.config ? makeConfigState(config.config) : defaultConfigConfig + + return { hosts, forwardings, autoconnects, autoforwards, config: configConfig } +} + +// state to object +const makeForwardingObject = (forwarding: ForwardingConfig, autoforward: AutoforwardConfig | null): ForwardingSchema => { + const result: any = {} + + if (forwarding.spec) { result.spec = serializeForwardingSpec(forwarding.spec) } + if (forwarding.label) { result.label = forwarding.label } + if (autoforward && autoforward.start) { result.autostart = true } + if (autoforward && autoforward.retry) { result.autoretry = true } + + if (!result.label && !result.autostart && !result.autoretry) { + return result.spec + } + + return result +} + +const makeSSHObject = (id: string, host: HostConfig) => { + const ssh = { ...(host.ssh) } + if (ssh.host === id) { delete ssh.host } + if (Object.keys(ssh.config).length === 0) { delete ssh.config } + + return ssh +} + +const makeHostObject = (id: string, host: HostConfig, forwardings: { id: string, fwdId: string, config: ForwardingConfig }[], autoconnect: AutoconnectConfig | null, autoforwards: { id: string, fwdId: string, config: AutoforwardConfig }[]): HostSchema => { + const ssh = makeSSHObject(id, host) + + const forward = forwardings.reduce((acc, forwarding) => { + const autoforward = find(autoforwards, { id, fwdId: forwarding.fwdId }) + acc[forwarding.fwdId] = makeForwardingObject(forwarding.config, autoforward ? autoforward.config : null) + return acc + }, {} as { [key: string]: ForwardingSchema }) + + const result: any = {} + + if (host.label) { result.label = host.label } + if (Object.keys(ssh).length > 0) { result.ssh = ssh } + if (Object.keys(forward).length > 0) { result.forward = forward } + if (autoconnect && autoconnect.start) { result.autostart = true } + if (autoconnect && autoconnect.retry) { result.autoretry = true } + + return Object.keys(result).length > 0 ? result : null +} + +const makeConfigObject = (config: ConfigConfig): ConfigConfigSchema => { + const result = { ...config } + if (!result.autosave) { + delete result.autosave + } + return config +} + +export const configStateToObject = (config: ConfigType): ConfigSchema => { + const hosts = config.hosts.reduce((acc, host) => { + const forwardings = config.forwardings.filter(x => x.id === host.id) + const autoconnect = find(config.autoconnects, { id: host.id }) + acc[host.id] = makeHostObject(host.id, host.config, forwardings, autoconnect ? autoconnect.config : null, config.autoforwards) + return acc + }, {} as { [key: string]: HostSchema }) + + const c = makeConfigObject(config.config) + + const result = { + hosts, + config: c + } + + if (Object.keys(result.config).length === 0) { delete result.config } + if (Object.keys(result.hosts).length === 0) { delete result.hosts } + + return result +} diff --git a/server/src/config/forwarding-spec.ts b/server/src/config/forwarding-spec.ts new file mode 100644 index 0000000..89acc88 --- /dev/null +++ b/server/src/config/forwarding-spec.ts @@ -0,0 +1,37 @@ +import { ForwardingSpec, fwdTypes } from '../forward' + +const token = '((?:\\S|(?:\\\\ ))+)' +const dynamicRegex = new RegExp(`^(?:D|dynamic) +${token}$`) +const localRegex = new RegExp(`^(?:L|local) +${token} +${token}$`) +const remoteRegex = new RegExp(`^(?:R|remote) +${token} +${token}$`) +const httpRegex = new RegExp(`^(?:H|http) +${token}$`) + +export const parseForwardingSpec = (str: string): ForwardingSpec => { + if (dynamicRegex.test(str)) { + const [/**/, bind] = str.match(dynamicRegex) || ['', ''] + return { type: fwdTypes.dynamic, bind } + } + if (localRegex.test(str)) { + const [/**/, bind, target] = str.match(localRegex) || ['', '', ''] + return { type: fwdTypes.local, bind, target } + } + if (remoteRegex.test(str)) { + const [/**/, bind, target] = str.match(remoteRegex) || ['', '', ''] + return { type: fwdTypes.remote, bind, target } + } + if (httpRegex.test(str)) { + const [/**/, target] = str.match(httpRegex) || ['', '', ''] + return { type: fwdTypes.http, target } + } + + throw Error(`invalid forwarding spec: ${JSON.stringify(str)}`) +} + +export const serializeForwardingSpec = (spec: ForwardingSpec): string => { + switch (spec.type) { + case fwdTypes.dynamic: return `D ${spec.bind}` + case fwdTypes.local: return `L ${spec.bind} ${spec.target}` + case fwdTypes.remote: return `R ${spec.bind} ${spec.target}` + case fwdTypes.http: return `H ${spec.target}` + } +} diff --git a/server/src/config/index.ts b/server/src/config/index.ts new file mode 100644 index 0000000..575d02b --- /dev/null +++ b/server/src/config/index.ts @@ -0,0 +1,4 @@ +export * from './actions' +export * from './component' +export * from './reducer' +export * from './types' diff --git a/server/src/config/reducer.ts b/server/src/config/reducer.ts new file mode 100644 index 0000000..5e57b2d --- /dev/null +++ b/server/src/config/reducer.ts @@ -0,0 +1,20 @@ +import { types } from './actions' +import { Action } from '../types/redux' +import { ConfigConfig } from './types' + +export type State = ConfigConfig + +const initialState = (): State => ({ + autosave: false +}) + +export const reducer = (state: State = initialState(), action: Action): State => { + switch (action.type) { + case types.CONFIG_EDIT: { + const { config } = action + return { ...state, ...config } + } + default: + return state + } +} diff --git a/server/src/config/schema.ts b/server/src/config/schema.ts new file mode 100644 index 0000000..79470f5 --- /dev/null +++ b/server/src/config/schema.ts @@ -0,0 +1,65 @@ +import { any, alternatives, boolean, object, string } from 'joi' + +const replaceIfNull = (schema: any, defoult: any) => alternatives().try( + schema, + any().valid(null).empty(null).default(defoult) +) + +const forwardingObject = object({ + spec: string().required(), + label: string(), + autostart: boolean(), + autoretry: boolean() +}) + +const forwarding = alternatives().try( + forwardingObject, + string() +) + +const host = object({ + ssh: object({ + host: string(), + config: object().pattern(/.*/, string()) + }), + forward: object().pattern(/.*/, forwarding), + label: string(), + autostart: boolean(), + autoretry: boolean() +}) + +const config = object({ + autosave: boolean() +}) + +export const configSchema = object({ + hosts: object().pattern(/.*/, replaceIfNull(host, {})), + config +}).default(null) // INFO hpello empty yaml file yields undefined + +export type ForwardingSchema = { + spec: string, + label?: string, + autostart?: boolean, + autoretry?: boolean +} | string + +export interface HostSchema { + ssh?: { + host?: string, + config?: { [key: string]: string } + }, + forward?: { [key: string]: ForwardingSchema }, + label?: string, + autostart?: boolean, + autoretry?: boolean +} + +export interface ConfigConfigSchema { + autosave?: boolean +} + +export type ConfigSchema = { + hosts?: { [key: string]: HostSchema }, + config?: ConfigConfigSchema +} | null diff --git a/server/src/config/serialize.ts b/server/src/config/serialize.ts new file mode 100644 index 0000000..1ac7ca9 --- /dev/null +++ b/server/src/config/serialize.ts @@ -0,0 +1,38 @@ +import { safeDump, safeLoad, JSON_SCHEMA } from 'js-yaml' +import { promisify } from 'util' +import { readFile, writeFile } from 'fs' + +import { configSchema, ConfigSchema } from './schema' +import { createLogger } from '../utils/log' + +const log = createLogger(__filename) + +const readFileAsync = promisify(readFile) +const writeFileAsync = promisify(writeFile) + +export const load = async (path: string): Promise => { + let data + try { + data = await readFileAsync(path) + } catch (err) { + log.error({ err }, 'error while reading config file:', path, { err }) + return null + } + + const object = safeLoad(data.toString(), { schema: JSON_SCHEMA }) + + const validation = configSchema.validate(object) + if (validation.error) { throw validation.error } + + return validation.value +} + +export const save = async (config: ConfigSchema, path: string): Promise => { + const validation = configSchema.validate(config) + if (validation.error) { log.error({ err: validation.error }, 'error while saving config'); return Promise.reject(validation.error) } + + const yaml = safeDump(config, { schema: JSON_SCHEMA }) + .replace(/\n {0,2}[^ ]/g, '\n$&') // improve readability + const header = '# Do not edit this file while sshmon is running.\n# It will be overwritten on config change.\n\n' + return writeFileAsync(path, `${header}${yaml}`) +} diff --git a/server/src/config/types.ts b/server/src/config/types.ts new file mode 100644 index 0000000..186debd --- /dev/null +++ b/server/src/config/types.ts @@ -0,0 +1,16 @@ +import { HostConfig } from '../host' +import { ForwardingConfig } from '../forward' +import { AutoconnectConfig } from '../autoconnect' +import { AutoforwardConfig } from '../autoforward' + +export type ConfigConfig = { + autosave: boolean +} + +export type ConfigType = { + hosts: { id: string, config: HostConfig }[], + forwardings: { id: string, fwdId: string, config: ForwardingConfig }[], + autoconnects: { id: string, config: AutoconnectConfig }[], + autoforwards: { id: string, fwdId: string, config: AutoforwardConfig }[], + config: ConfigConfig +} diff --git a/server/src/engine.ts b/server/src/engine.ts new file mode 100644 index 0000000..fe55901 --- /dev/null +++ b/server/src/engine.ts @@ -0,0 +1,59 @@ +import { API } from './api' +import { Config } from './config' +import { Forwarder } from './forward' +import { Autoconnector } from './autoconnect' +import { Autoforwarder } from './autoforward' +import { store } from './store' +import { System } from './system' +import { disconnectAllHosts } from './utils/disconnect-all-hosts' +import { setupGracefulShutdown } from './utils/graceful-shutdown' +import { createLogger } from './utils/log' + +const log = createLogger(__filename) + +export interface EngineOptions { + configFile?: string +} + +export class Engine { + system: System + forwarder: Forwarder + autoconnector: Autoconnector + autoforwarder: Autoforwarder + config: Config + api: API + + constructor(options: EngineOptions) { + this.system = new System({ store }) + this.forwarder = new Forwarder({ store }) + this.autoconnector = new Autoconnector({ store }) + this.autoforwarder = new Autoforwarder({ store }) + this.config = new Config({ store, configPath: options.configFile || null }) + this.api = new API({ store }) + } + + async start() { + this.system.setup() + this.forwarder.setup() + this.autoconnector.setup() + this.autoforwarder.setup() + await this.config.setup() + + this.api.setup() + this.api.listen(8377, 'localhost') + } + + async stop() { + log.info('shutting down api...') + await this.api.shutdown() + log.info('disconnecting all hosts...') + await disconnectAllHosts(store) + log.info('engine stopped') + } +} + +export const start = (options: EngineOptions) => { + const engine = new Engine(options) + engine.start() + setupGracefulShutdown(() => engine.stop()) +} diff --git a/server/src/forward/actions.ts b/server/src/forward/actions.ts new file mode 100644 index 0000000..cd16c49 --- /dev/null +++ b/server/src/forward/actions.ts @@ -0,0 +1,58 @@ +import { ForwardingConfig, ForwardingParams } from './types' + +export enum types { + FORWARDING_CREATE = 'FORWARDING_CREATE', + FORWARDING_EDIT = 'FORWARDING_EDIT', + FORWARDING_DELETE = 'FORWARDING_DELETE', + FORWARDING_STATE_CHANGE = 'FORWARDING_STATE_CHANGE' +} + +export type ForwardingStatus = 'connecting' | 'connected' | 'disconnecting' | 'disconnected' | 'error' + +interface ForwardingCreateAction { + type: types.FORWARDING_CREATE, + id: string, + fwdId: string, + config: ForwardingConfig +} + +interface ForwardingEditAction { + type: types.FORWARDING_EDIT, + id: string, + fwdId: string, + config: ForwardingConfig +} + +interface ForwardingDeleteAction { + type: types.FORWARDING_DELETE, + id: string + fwdId: string +} + +interface ForwardingStateChangeAction { + type: types.FORWARDING_STATE_CHANGE, + id: string, + fwdId: string, + status: ForwardingStatus, + params?: ForwardingParams, + reason?: string +} + +export type Action = + | ForwardingCreateAction + | ForwardingEditAction + | ForwardingDeleteAction + | ForwardingStateChangeAction + + +export const actions = { + forwardingCreate: (id: string, fwdId: string, config: ForwardingConfig): Action => ({ type: types.FORWARDING_CREATE, id, fwdId, config }), + forwardingEdit: (id: string, fwdId: string, config: ForwardingConfig): Action => ({ type: types.FORWARDING_EDIT, id, fwdId, config }), + forwardingDelete: (id: string, fwdId: string): Action => ({ type: types.FORWARDING_DELETE, id, fwdId }), + + forwardingConnecting: (id: string, fwdId: string, params: ForwardingParams, reason: string): Action => ({ type: types.FORWARDING_STATE_CHANGE, id, fwdId, status: 'connecting', params, reason }), + forwardingConnected: (id: string, fwdId: string): Action => ({ type: types.FORWARDING_STATE_CHANGE, id, fwdId, status: 'connected' }), + forwardingDisconnecting: (id: string, fwdId: string, reason: string): Action => ({ type: types.FORWARDING_STATE_CHANGE, id, fwdId, status: 'disconnecting', reason }), + forwardingDisconnected: (id: string, fwdId: string): Action => ({ type: types.FORWARDING_STATE_CHANGE, id, fwdId, status: 'disconnected' }), + forwardingError: (id: string, fwdId: string): Action => ({ type: types.FORWARDING_STATE_CHANGE, id, fwdId, status: 'error' }) +} diff --git a/server/src/forward/component.ts b/server/src/forward/component.ts new file mode 100644 index 0000000..5bd5c57 --- /dev/null +++ b/server/src/forward/component.ts @@ -0,0 +1,23 @@ +import { State, Store } from '../types/redux' +import { onStateChange } from './utils' + +export class Forwarder { + store: Store + prevState: State + + constructor(params: { store: Store }) { + this.store = params.store + this.prevState = params.store.getState() + } + + setup() { + this.store.subscribe(() => { + const state = this.store.getState() + + process.nextTick(() => { // prevent recursion + onStateChange(this.prevState, state, this.store.dispatch) + this.prevState = state + }) + }) + } +} diff --git a/server/src/forward/index.ts b/server/src/forward/index.ts new file mode 100644 index 0000000..2ecd8dd --- /dev/null +++ b/server/src/forward/index.ts @@ -0,0 +1,5 @@ +export * from './actions' +export * from './component' +export * from './reducer' +export * from './thunks' +export * from './types' diff --git a/server/src/forward/reducer.ts b/server/src/forward/reducer.ts new file mode 100644 index 0000000..76fa885 --- /dev/null +++ b/server/src/forward/reducer.ts @@ -0,0 +1,68 @@ +import { ForwardingStatus, types } from './actions' +import { ForwardingParams, ForwardingConfig } from './types' +import { Action } from '../types/redux' + +export type ForwardingSubState = { + status: ForwardingStatus, + params: ForwardingParams | null, + reason: string | null +} + +export interface ForwardingState { + id: string, + fwdId: string, + config: ForwardingConfig, + state: ForwardingSubState +} + +export type State = ForwardingState[] + +const initialState = (): State => ([]) + +const defaultSubState = (): ForwardingSubState => ({ + status: 'disconnected', + params: null, + reason: null +}) + +export const reducer = (state: State = initialState(), action: Action): State => { + switch (action.type) { + case types.FORWARDING_CREATE: { + const { id, fwdId, config } = action + + const forwardingState = { + id, + fwdId, + config, + state: defaultSubState() + } + + return [...state, forwardingState] + } + case types.FORWARDING_EDIT: { + const { id, fwdId, config } = action + return state.map(x => x.id === id && x.fwdId === fwdId ? ({ ...x, config }) : x) + } + case types.FORWARDING_DELETE: { + const { id, fwdId } = action + return state.filter(x => x.id !== id || x.fwdId !== fwdId) + } + case types.FORWARDING_STATE_CHANGE: { + const { id, fwdId, status, params, reason } = action + return state.map((forwardingState) => { + if (forwardingState.id !== id || forwardingState.fwdId !== fwdId) { return forwardingState } + + return { + ...forwardingState, + state: { + status, + params: params || forwardingState.state.params, + reason: reason || forwardingState.state.reason + } + } + }) + } + default: + return state + } +} diff --git a/server/src/forward/ssh.ts b/server/src/forward/ssh.ts new file mode 100644 index 0000000..c300c16 --- /dev/null +++ b/server/src/forward/ssh.ts @@ -0,0 +1,48 @@ +import { spawn } from 'child_process' + +import { ForwardingParams, fwdTypes } from './types' +import { createLogger } from '../utils/log' + +const log = createLogger(__filename) + +const makeForwardingString = (params: ForwardingParams): string => { + switch (params.type) { + case fwdTypes.dynamic: return `-D${params.bind}` + case fwdTypes.local: return `-L${params.bind}:${params.target}` + case fwdTypes.remote: return `-R${params.bind}:${params.target}` + } +} + +function appendMulti(array1: T[], array2: T[]) { + Array.prototype.push.apply(array1, array2) +} + +// FIXME hpello duplicated code +function spawnAndLog(sshCommand: string, args: string[]) { + const process = spawn(sshCommand, args, { detached: true }) + + const processLog = log.child({ childPid: process.pid }) + processLog.debug([sshCommand].concat(args).join(' ')) + process.stdout.on('data', data => processLog.debug({ stream: 'stdout' }, data.toString().trim())) + process.stderr.on('data', data => processLog.error({ stream: 'stderr' }, data.toString().trim())) + process.on('error', err => processLog.error({ err, event: 'error' })) + process.on('exit', (code, signal) => processLog.debug({ event: 'exit', code, signal }, 'process exited')) + + return process +} + +type ControlCommand = 'check' | 'forward' | 'cancel' | 'exit' | 'stop' + +export function executeSshControlCommand(params: { sshCommand: string, controlPath: string, controlCommand: ControlCommand, forwardingParams: ForwardingParams | null }) { + const { sshCommand, controlPath, controlCommand, forwardingParams } = params + + const args = [] as string[] + appendMulti(args, ['-S', controlPath]) + appendMulti(args, ['-O', controlCommand]) + if (forwardingParams !== null) { + appendMulti(args, [makeForwardingString(forwardingParams)]) + } + args.push('sshmon-host') + + return spawnAndLog(sshCommand, args) +} diff --git a/server/src/forward/thunks/connect-thunks.ts b/server/src/forward/thunks/connect-thunks.ts new file mode 100644 index 0000000..aa21ba1 --- /dev/null +++ b/server/src/forward/thunks/connect-thunks.ts @@ -0,0 +1,95 @@ +import { find } from 'lodash' + +import { actions } from '../actions' +import { executeSshControlCommand } from '../ssh' +import { ForwardingSpec, ForwardingParams, fwdTypes } from '../types' +import { AsyncThunkAction, Dispatch, GetState } from '../../types/redux' +import { ErrorWithCode } from '../../utils/error-with-code' +import { createLogger } from '../../utils/log' +import { makeTmpPath } from '../../utils/tmp' + +const log = createLogger(__filename) + +// FIXME hpello put this outside and add some tests +const defaultLocalHost = 'localhost' +const defaultRemoteHost = 'localhost' + +const makeAddress = (address: string, defaultHost: string) => { + // unix socket + if (address.includes('/')) { return address } + + // host:port + if (address.includes(':')) { return address } + + const port = parseInt(address, 10) + if (!isNaN(port)) { return `${defaultHost}:${port}` } + + return null +} + +const makeForwardingParams = async (id: string, fwdId: string, spec: ForwardingSpec): Promise => { + switch (spec.type) { + case fwdTypes.dynamic: return spec + case fwdTypes.local: { + const target = makeAddress(spec.target, defaultRemoteHost) + if (target === null) { throw new ErrorWithCode(400, `invalid forwarding spec: ${JSON.stringify(spec)}`) } + return { ...spec, target } + } + case fwdTypes.remote: { + const target = makeAddress(spec.target, defaultLocalHost) + if (target === null) { throw new ErrorWithCode(400, `invalid forwarding spec: ${JSON.stringify(spec)}`) } + return { ...spec, target } + } + case fwdTypes.http: { + const bind = await makeTmpPath(__filename)(JSON.stringify({ id, fwdId })) + const target = makeAddress(spec.target, defaultRemoteHost) + if (target === null) { throw new ErrorWithCode(400, `invalid forwarding spec: ${JSON.stringify(spec)}`) } + return { type: fwdTypes.local, bind, target } + } + } +} + +export const forwardingConnect = (id: string, fwdId: string, reason: string): AsyncThunkAction => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const forwarding = find(state.forwardings, { id, fwdId }) + if (!forwarding) { throw new ErrorWithCode(404, `forwarding not found: ${id}/${fwdId}`) } + + const h = find(state.hosts, { id }) + if (!h) { throw new ErrorWithCode(404, `host not found: ${id}`) } + + if (h.state.status !== 'connected') { throw new ErrorWithCode(400, `host is not connected: ${id}`) } + if (h.state.controlPath === null) { throw new ErrorWithCode(500, `bad state: ${id} (${JSON.stringify(h.state)})`) } + + const params = await makeForwardingParams(id, fwdId, forwarding.config.spec) + + const process = executeSshControlCommand({ sshCommand: 'ssh', controlPath: h.state.controlPath, controlCommand: 'forward', forwardingParams: params }) + dispatch(actions.forwardingConnecting(id, fwdId, params, reason)) + + process.on('exit', (code) => { + dispatch((code === 0) ? actions.forwardingConnected(id, fwdId) : actions.forwardingError(id, fwdId)) + }) +} + +export const forwardingDisconnect = (id: string, fwdId: string, reason: string): AsyncThunkAction => (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const forwarding = find(state.forwardings, { id, fwdId }) + if (!forwarding) { throw new ErrorWithCode(404, `forwarding not found: ${id}/${fwdId}`) } + + const h = find(state.hosts, { id }) + if (!h) { throw new ErrorWithCode(404, `host not found: ${id}`) } + + if (h.state.status !== 'connected') { throw new ErrorWithCode(400, `host is not connected: ${id}`) } + if (h.state.controlPath === null) { throw new ErrorWithCode(500, `bad state: ${id} (${JSON.stringify(h.state)})`) } + + if (!['connecting', 'connected'].includes(forwarding.state.status)) { log.info(`forwarding is not connected: ${id}/${fwdId}`); return Promise.resolve() } + if (forwarding.state.params === null) { throw new ErrorWithCode(500, `bad state: ${id}/${fwdId} (${JSON.stringify(forwarding.state)})`) } + + const process = executeSshControlCommand({ sshCommand: 'ssh', controlPath: h.state.controlPath, controlCommand: 'cancel', forwardingParams: forwarding.state.params }) + dispatch(actions.forwardingDisconnecting(id, fwdId, reason)) + + process.on('exit', (code) => { + dispatch((code === 0) ? actions.forwardingDisconnected(id, fwdId) : actions.forwardingError(id, fwdId)) + }) + + return Promise.resolve() +} diff --git a/server/src/forward/thunks/create-thunks.ts b/server/src/forward/thunks/create-thunks.ts new file mode 100644 index 0000000..ef45197 --- /dev/null +++ b/server/src/forward/thunks/create-thunks.ts @@ -0,0 +1,39 @@ +import { find } from 'lodash' + +import { actions } from '../actions' +import { ForwardingConfig } from '../types' +import { AsyncThunkAction, Dispatch, GetState } from '../../types/redux' +import { ErrorWithCode } from '../../utils/error-with-code' +import { createLogger } from '../../utils/log' + +const log = createLogger(__filename) + +export const forwardingCreate = (id: string, fwdId: string, config: ForwardingConfig): AsyncThunkAction => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const h = find(state.hosts, { id }) + if (!h) { log.info(`host not found: ${id}`); return } + + const forwarding = find(state.forwardings, { id, fwdId }) + if (forwarding) { throw new ErrorWithCode(409, `forwarding already exists: ${id}/${fwdId}`) } + + dispatch(actions.forwardingCreate(id, fwdId, config)) +} + +export const forwardingEdit = (id: string, fwdId: string, config: ForwardingConfig): AsyncThunkAction => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const h = find(state.hosts, { id }) + if (!h) { log.info(`host not found: ${id}`); return } + + const forwarding = find(state.forwardings, { id, fwdId }) + if (!forwarding) { throw new ErrorWithCode(404, `forwarding not found: ${id}/${fwdId}`) } + + dispatch(actions.forwardingEdit(id, fwdId, config)) +} + +export const forwardingDelete = (id: string, fwdId: string): AsyncThunkAction => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const forwarding = find(state.forwardings, { id, fwdId }) + if (!forwarding) { log.info(`forwarding not found: ${id}/${fwdId}`); return } + + dispatch(actions.forwardingDelete(id, fwdId)) +} diff --git a/server/src/forward/thunks/index.ts b/server/src/forward/thunks/index.ts new file mode 100644 index 0000000..a1da6e2 --- /dev/null +++ b/server/src/forward/thunks/index.ts @@ -0,0 +1,10 @@ +import { forwardingConnect, forwardingDisconnect } from './connect-thunks' +import { forwardingCreate, forwardingEdit, forwardingDelete } from './create-thunks' + +export const thunks = { + forwardingCreate, + forwardingEdit, + forwardingDelete, + forwardingConnect, + forwardingDisconnect +} diff --git a/server/src/forward/types.ts b/server/src/forward/types.ts new file mode 100644 index 0000000..3308220 --- /dev/null +++ b/server/src/forward/types.ts @@ -0,0 +1,20 @@ +export enum fwdTypes { + dynamic = 'dynamic', + local = 'local', + remote = 'remote', + http = 'http' +} + +export type ForwardingSpec = + | { type: fwdTypes.dynamic, bind: string } + | { type: fwdTypes.local | fwdTypes.remote, bind: string, target: string } + | { type: fwdTypes.http, target: string } + +export type ForwardingConfig = { + spec: ForwardingSpec, + label: string +} + +export type ForwardingParams = + | { type: fwdTypes.dynamic, bind: string } + | { type: fwdTypes.local | fwdTypes.remote, bind: string, target: string } diff --git a/server/src/forward/utils.ts b/server/src/forward/utils.ts new file mode 100644 index 0000000..e9e795c --- /dev/null +++ b/server/src/forward/utils.ts @@ -0,0 +1,53 @@ +import { find, flatten, some } from 'lodash' + +import { actions } from './actions' +import { HostStatus } from '../host' +import { ForwardingState } from './reducer' +import { Dispatch, State } from '../types/redux' + +const getStatus = (state: State, id: string): HostStatus | null => { + const host = find(state.hosts, { id }) + return !host ? null : host.state.status +} + +const getForwardings = (state: State, id: string): ForwardingState[] => { + return state.forwardings.filter(x => x.id === id) +} + +const hostExist = (state: State, id: string): boolean => some(state.hosts, { id }) + +const cancelForwardings = (prevState: State, state: State, dispatch: Dispatch) => { + flatten( + state.hosts.map(x => x.id) + .filter(id => getStatus(prevState, id) !== 'disconnected') + .filter(id => getStatus(state, id) === 'disconnected') + .map(id => getForwardings(state, id)) + ).forEach(({ id, fwdId }) => { + dispatch(actions.forwardingDisconnected(id, fwdId)) + }) + + flatten( + state.hosts.map(x => x.id) + .filter(id => getStatus(prevState, id) !== 'error') + .filter(id => getStatus(state, id) === 'error') + .map(id => getForwardings(state, id)) + ).forEach(({ id, fwdId }) => { + dispatch(actions.forwardingDisconnected(id, fwdId)) + }) +} + +const cleanupForwardings = (prevState: State, state: State, dispatch: Dispatch) => { + flatten( + state.hosts.map(x => x.id) + .filter(id => hostExist(prevState, id)) + .filter(id => !hostExist(state, id)) + .map(id => getForwardings(state, id)) + ).forEach(({ id, fwdId }) => { + dispatch(actions.forwardingDelete(id, fwdId)) + }) +} + +export const onStateChange = (prevState: State, state: State, dispatch: Dispatch) => { + cancelForwardings(prevState, state, dispatch) + cleanupForwardings(prevState, state, dispatch) +} diff --git a/server/src/host/actions.ts b/server/src/host/actions.ts new file mode 100644 index 0000000..4e3244b --- /dev/null +++ b/server/src/host/actions.ts @@ -0,0 +1,55 @@ +import { HostConfig } from './types' + +export enum types { + HOST_CREATE = 'HOST_CREATE', + HOST_EDIT = 'HOST_EDIT', + HOST_DELETE = 'HOST_DELETE', + HOST_STATE_CHANGE = 'HOST_STATE_CHANGE' +} + +export type HostStatus = 'connecting' | 'connected' | 'disconnecting' | 'disconnected' | 'error' + +interface HostCreateAction { + type: types.HOST_CREATE, + id: string, + config: HostConfig +} + +interface HostEditAction { + type: types.HOST_EDIT, + id: string, + config: HostConfig +} + +interface HostDeleteAction { + type: types.HOST_DELETE, + id: string +} + +interface HostStateChangeAction { + type: types.HOST_STATE_CHANGE, + id: string, + status: HostStatus, + pid?: number, + controlPath?: string, + time?: Date, + reason?: string +} + +export type Action = + | HostCreateAction + | HostEditAction + | HostDeleteAction + | HostStateChangeAction + +export const actions = { + hostCreate: (id: string, config: HostConfig): Action => ({ type: types.HOST_CREATE, id, config }), + hostEdit: (id: string, config: HostConfig): Action => ({ type: types.HOST_EDIT, id, config }), + hostDelete: (id: string): Action => ({ type: types.HOST_DELETE, id }), + + hostStateConnecting: (id: string, pid: number, controlPath: string, reason: string): Action => ({ type: types.HOST_STATE_CHANGE, id, status: 'connecting', pid, controlPath, reason }), + hostStateConnected: (id: string, time: Date): Action => ({ type: types.HOST_STATE_CHANGE, id, status: 'connected', time }), + hostStateDisconnecting: (id: string, reason: string): Action => ({ type: types.HOST_STATE_CHANGE, id, status: 'disconnecting', reason }), + hostStateDisconnected: (id: string): Action => ({ type: types.HOST_STATE_CHANGE, id, status: 'disconnected' }), + hostStateError: (id: string): Action => ({ type: types.HOST_STATE_CHANGE, id, status: 'error' }) +} diff --git a/server/src/host/index.ts b/server/src/host/index.ts new file mode 100644 index 0000000..228d0d7 --- /dev/null +++ b/server/src/host/index.ts @@ -0,0 +1,4 @@ +export * from './actions' +export * from './reducer' +export * from './thunks' +export * from './types' diff --git a/server/src/host/reducer.ts b/server/src/host/reducer.ts new file mode 100644 index 0000000..c223364 --- /dev/null +++ b/server/src/host/reducer.ts @@ -0,0 +1,73 @@ +import { HostStatus, types } from './actions' +import { HostConfig } from './types' +import { Action } from '../types/redux' + +export interface HostSubState { + status: HostStatus, + pid: number | null, + controlPath: string | null, + time: Date | null, + reason: string | null +} + +export interface HostState { + id: string, + config: HostConfig, + state: HostSubState +} + +export type State = HostState[] + +const initialState = (): State => ([]) + +const defaultSubState = (): HostSubState => ({ + status: 'disconnected', + pid: null, + controlPath: null, + time: null, + reason: null +}) + +export const reducer = (state: State = initialState(), action: Action): State => { + switch (action.type) { + case types.HOST_CREATE: { + const { id, config } = action + + const hostState = { + id, + config, + state: defaultSubState() + } + + return [...state, hostState] + } + case types.HOST_EDIT: { + const { id, config } = action + return state.map(x => x.id === id ? ({ ...x, config }) : x) + } + case types.HOST_DELETE: { + const { id } = action + return state.filter(x => x.id !== id) + } + case types.HOST_STATE_CHANGE: { + const { id, status, pid, controlPath, time, reason } = action + + return state.map((hostState) => { + if (hostState.id !== id) { return hostState } + + return { + ...hostState, + state: { + status, + pid: pid || hostState.state.pid, + controlPath: controlPath || hostState.state.controlPath, + time: time || hostState.state.time, + reason: reason || hostState.state.reason + } + } + }) + } + default: + return state + } +} diff --git a/server/src/host/ssh.ts b/server/src/host/ssh.ts new file mode 100644 index 0000000..fceaebb --- /dev/null +++ b/server/src/host/ssh.ts @@ -0,0 +1,56 @@ +import { spawn } from 'child_process' + +import { createLogger } from '../utils/log' + +const log = createLogger(__filename) + +interface SSHParams { + host: string, + config: { [key: string]: string } +} + +const defaultMasterOptions = [ + '-N', + '-T', + '-M', + '-o', 'ControlPersist=no', + '-o', 'BatchMode=yes', + '-o', 'StreamLocalBindUnlink=yes', + '-o', 'ServerAliveInterval=5', + '-o', 'ServerAliveCountMax=3' +] + +function appendMulti(array1: T[], array2: T[]) { + Array.prototype.push.apply(array1, array2) +} + +// FIXME hpello duplicated code +function spawnAndLog(sshCommand: string, args: string[]) { + const process = spawn(sshCommand, args, { detached: true }) + + const processLog = log.child({ childPid: process.pid }) + processLog.debug([sshCommand].concat(args).join(' ')) + process.stdout.on('data', data => processLog.debug({ stream: 'stdout' }, data.toString().trim())) + process.stderr.on('data', data => processLog.error({ stream: 'stderr' }, data.toString().trim())) + process.on('error', err => processLog.error({ err, event: 'error' })) + process.on('exit', (code, signal) => processLog.debug({ event: 'exit', code, signal }, 'process exited')) + + return process +} + +export function spawnSshMaster(params: { sshCommand: string, controlPath: string, sshParams: SSHParams }) { + const { sshCommand, controlPath, sshParams } = params + + const args = defaultMasterOptions.concat() + appendMulti(args, ['-S', controlPath]) + + const { host, config } = sshParams + Object.keys(config) + .filter(k => k.toLowerCase() !== 'controlpath') + .forEach((key) => { + appendMulti(args, ['-o', `${key}=${config[key]}`]) + }) + args.push(host) + + return spawnAndLog(sshCommand, args) +} diff --git a/server/src/host/thunks/connect-thunks.ts b/server/src/host/thunks/connect-thunks.ts new file mode 100644 index 0000000..93b1dd1 --- /dev/null +++ b/server/src/host/thunks/connect-thunks.ts @@ -0,0 +1,68 @@ +import { access } from 'fs' +import { watch } from 'chokidar' +import { find } from 'lodash' +import { promisify } from 'util' + +import { actions } from '../actions' +import { spawnSshMaster } from '../ssh' +import { HostConfig } from '../types' +import { AsyncThunkAction, Dispatch, GetState } from '../../types/redux' +import { ErrorWithCode } from '../../utils/error-with-code' +import { createLogger } from '../../utils/log' +import { makeTmpPath } from '../../utils/tmp' + +const log = createLogger(__filename) + +const extractConfigControlPath = (hostConfig: HostConfig): string | null => { + const sshConfig = hostConfig.ssh.config + const key = Object.keys(sshConfig).find(k => k.toLowerCase() === 'controlpath') + return key ? sshConfig[key] : null +} + +const accessAsync = promisify(access) +const fileExists = (path: string): Promise => accessAsync(path).then(() => true).catch(() => false) + +export const hostConnect = (id: string, reason: string): AsyncThunkAction => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const host = find(state.hosts, { id }) + if (!host) { throw new ErrorWithCode(404, `host not found: ${id}`) } + + if (!['disconnected', 'error'].includes(host.state.status)) { log.info(`host not down: ${id}`); return } + + const controlPath = extractConfigControlPath(host.config) || await makeTmpPath(__filename)(id) + + if (await fileExists(controlPath)) { + log.error(`control path already exists: ${controlPath}`) + dispatch(actions.hostStateError(id)) + return + } + + const process = spawnSshMaster({ sshCommand: 'ssh', controlPath, sshParams: host.config.ssh }) + dispatch(actions.hostStateConnecting(id, process.pid, controlPath, reason)) + + const watcher = watch(controlPath, { useFsEvents: false }) + .on('error', err => log.error({ err }, 'watcher error')) + .once('add', () => { + watcher.close() + dispatch(actions.hostStateConnected(id, new Date())) + }) + + process.on('exit', (code) => { + watcher.close() + dispatch((code === 0 || code === null) ? actions.hostStateDisconnected(id) : actions.hostStateError(id)) + }) +} + +export const hostDisconnect = (id: string, reason: string): AsyncThunkAction => (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const host = find(state.hosts, { id }) + if (!host) { throw new ErrorWithCode(404, `host not found: ${id}`) } + + if (!['connecting', 'connected'].includes(host.state.status)) { log.info('host is not connected:', id); return Promise.resolve() } + if (host.state.pid === null) { throw new ErrorWithCode(500, `bad state: ${id} (${JSON.stringify(host.state)})`) } + + process.kill(host.state.pid) + dispatch(actions.hostStateDisconnecting(id, reason)) + + return Promise.resolve() +} diff --git a/server/src/host/thunks/create-thunks.ts b/server/src/host/thunks/create-thunks.ts new file mode 100644 index 0000000..5bd6512 --- /dev/null +++ b/server/src/host/thunks/create-thunks.ts @@ -0,0 +1,34 @@ +import { find } from 'lodash' + +import { actions } from '../actions' +import { HostConfig } from '../types' +import { AsyncThunkAction, Dispatch, GetState } from '../../types/redux' +import { ErrorWithCode } from '../../utils/error-with-code' +import { createLogger } from '../../utils/log' + +const log = createLogger(__filename) + +export const hostCreate = (id: string, config: HostConfig): AsyncThunkAction => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const host = find(state.hosts, { id }) + if (host) { throw new ErrorWithCode(409, `host already exists: ${id}`) } + + dispatch(actions.hostCreate(id, config)) +} + +export const hostEdit = (id: string, config: HostConfig): AsyncThunkAction => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const host = find(state.hosts, { id }) + if (!host) { throw new ErrorWithCode(404, `host not found: ${id}`) } + + dispatch(actions.hostEdit(id, config)) +} + +export const hostDelete = (id: string): AsyncThunkAction => async (dispatch: Dispatch, getState: GetState) => { + const state = getState() + const host = find(state.hosts, { id }) + if (!host) { log.info(`host not found: ${id}`); return } + if (!['disconnected', 'error'].includes(host.state.status)) { throw new ErrorWithCode(400, `host not down: ${id}`) } + + dispatch(actions.hostDelete(id)) +} diff --git a/server/src/host/thunks/index.ts b/server/src/host/thunks/index.ts new file mode 100644 index 0000000..0fa57de --- /dev/null +++ b/server/src/host/thunks/index.ts @@ -0,0 +1,10 @@ +import { hostConnect, hostDisconnect } from './connect-thunks' +import { hostCreate, hostEdit, hostDelete } from './create-thunks' + +export const thunks = { + hostCreate, + hostEdit, + hostDelete, + hostConnect, + hostDisconnect +} diff --git a/server/src/host/types.ts b/server/src/host/types.ts new file mode 100644 index 0000000..623f41b --- /dev/null +++ b/server/src/host/types.ts @@ -0,0 +1,7 @@ +export type HostConfig = { + ssh: { + host: string, + config: { [key: string]: string } + }, + label: string +} diff --git a/server/src/reducer.ts b/server/src/reducer.ts new file mode 100644 index 0000000..2ecf908 --- /dev/null +++ b/server/src/reducer.ts @@ -0,0 +1,35 @@ +import { combineReducers } from 'redux' + +import { Action as HostAction, reducer as hosts, State as HostState } from './host' +import { Action as ForwardAction, reducer as forwardings, State as ForwardState } from './forward' +import { Action as AutoconnectAction, reducer as autoconnects, State as AutoconnectState } from './autoconnect' +import { Action as AutoforwardAction, reducer as autoforwards, State as AutoforwardState } from './autoforward' +import { Action as SystemAction, reducer as system, State as SystemState } from './system' +import { Action as ConfigAction, reducer as config, State as ConfigState } from './config' + +export type State = { + hosts: HostState, + forwardings: ForwardState, + autoconnects: AutoconnectState, + autoforwards: AutoforwardState, + system: SystemState, + config: ConfigState +} + +export type Action = + | HostAction + | ForwardAction + | AutoconnectAction + | AutoforwardAction + | SystemAction + | ConfigAction + | { type: '__any_other_action_type__' } + +export const reducer = combineReducers({ + hosts, + forwardings, + autoconnects, + autoforwards, + system, + config +} as any) // FIXME hpello https://github.com/reactjs/redux/issues/2709 diff --git a/server/src/store.ts b/server/src/store.ts new file mode 100644 index 0000000..d8bfcfe --- /dev/null +++ b/server/src/store.ts @@ -0,0 +1,18 @@ +import { applyMiddleware, createStore } from 'redux' +import thunk from 'redux-thunk' + +import { reducer } from './reducer' +import { logMiddleware } from './utils/log-middleware' + +const makeMiddleware = () => { + if (process.env.NODE_ENV === 'production') { + return applyMiddleware(thunk) + } else { // tslint:disable-line no-else-after-return + return applyMiddleware(thunk, logMiddleware) + } +} + +export const store = createStore( + reducer, + makeMiddleware() +) diff --git a/server/src/system/actions.ts b/server/src/system/actions.ts new file mode 100644 index 0000000..7c09abb --- /dev/null +++ b/server/src/system/actions.ts @@ -0,0 +1,25 @@ +import { SystemInfo, SystemStats } from './types' + +export enum types { + SYSTEM_ADD_INFO = 'SYSTEM_ADD_INFO', + SYSTEM_ADD_STATS = 'SYSTEM_ADD_STATS' +} + +interface SystemAddInfoAction { + type: types.SYSTEM_ADD_INFO, + info: SystemInfo +} + +interface SystemAddStatsAction { + type: types.SYSTEM_ADD_STATS, + stats: SystemStats +} + +export type Action = + | SystemAddInfoAction + | SystemAddStatsAction + +export const actions = { + systemAddInfo: (info: SystemInfo): Action => ({ type: types.SYSTEM_ADD_INFO, info }), + systemAddStats: (stats: SystemStats): Action => ({ type: types.SYSTEM_ADD_STATS, stats }) +} diff --git a/server/src/system/component.ts b/server/src/system/component.ts new file mode 100644 index 0000000..9a40c9e --- /dev/null +++ b/server/src/system/component.ts @@ -0,0 +1,26 @@ +import { Store } from '../types/redux' + +import { actions } from './actions' +import { getSystemInfo, getSystemStats } from './utils' + +const SYSTEM_GET_STATS_INTERVAL = 10 * 1000 + +export class System { + store: Store + + constructor(params: { store: Store }) { + this.store = params.store + } + + setup() { + const info = getSystemInfo() + this.store.dispatch(actions.systemAddInfo(info)) + + const startTime = Date.now() + getSystemStats(startTime).then(stats => this.store.dispatch(actions.systemAddStats(stats))) + setInterval( + () => getSystemStats(startTime).then(stats => this.store.dispatch(actions.systemAddStats(stats))), + SYSTEM_GET_STATS_INTERVAL + ) + } +} diff --git a/server/src/system/index.ts b/server/src/system/index.ts new file mode 100644 index 0000000..5206f78 --- /dev/null +++ b/server/src/system/index.ts @@ -0,0 +1,3 @@ +export * from './actions' +export * from './component' +export * from './reducer' diff --git a/server/src/system/reducer.ts b/server/src/system/reducer.ts new file mode 100644 index 0000000..641bfaf --- /dev/null +++ b/server/src/system/reducer.ts @@ -0,0 +1,30 @@ +import { types } from './actions' +import { SystemInfo, SystemStats } from './types' +import { Action } from '../types/redux' + +export interface SystemState { + info: SystemInfo | null, + stats: SystemStats | null +} + +export type State = SystemState + +const initialState = (): State => ({ + info: null, + stats: null +}) + +export const reducer = (state: State = initialState(), action: Action): State => { + switch (action.type) { + case types.SYSTEM_ADD_INFO: { + const { info } = action + return { ...state, info } + } + case types.SYSTEM_ADD_STATS: { + const { stats } = action + return { ...state, stats } + } + default: + return state + } +} diff --git a/server/src/system/types.ts b/server/src/system/types.ts new file mode 100644 index 0000000..caf2bd8 --- /dev/null +++ b/server/src/system/types.ts @@ -0,0 +1,18 @@ +export type SystemInfo = { + arch: string, + homeDir: string, + hostName: string, + nodeVersion: string, + pid: number, + platform: string, + totalCPUs: number + totalMemoryBytes: number + user: string, + version: string +} + +export type SystemStats = { + cpuUsage: number, + memoryUsageBytes: number + uptimeSeconds: number +} diff --git a/server/src/system/utils.ts b/server/src/system/utils.ts new file mode 100644 index 0000000..5068889 --- /dev/null +++ b/server/src/system/utils.ts @@ -0,0 +1,35 @@ +import * as os from 'os' + +import { SystemInfo, SystemStats } from './types' + +export const getSystemInfo = (): SystemInfo => { + const arch = os.arch() + const hostName = os.hostname() + const homeDir = os.homedir() + const totalCPUs = os.cpus().length + const totalMemoryBytes = os.totalmem() + const user = os.userInfo().username + + const { pid, platform } = process + const nodeVersion = process.version + + const { version } = require('../../../package.json') + + return { arch, homeDir, hostName, nodeVersion, pid, platform, totalCPUs, totalMemoryBytes, user, version } +} + +const setTimeoutAsync = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout)) + +const CPU_USAGE_WINDOW = 1000 + +export const getSystemStats = async (startTime: number): Promise => { + const startUsageTime = process.cpuUsage() + await setTimeoutAsync(CPU_USAGE_WINDOW) + const usage = process.cpuUsage(startUsageTime) + + const cpuUsage = (usage.system + usage.user) / CPU_USAGE_WINDOW + const memoryUsageBytes = process.memoryUsage().rss + const uptimeSeconds = (Date.now() - startTime) / 1000 + + return { cpuUsage, memoryUsageBytes, uptimeSeconds } +} diff --git a/server/src/types/redux.d.ts b/server/src/types/redux.d.ts new file mode 100644 index 0000000..f23fea1 --- /dev/null +++ b/server/src/types/redux.d.ts @@ -0,0 +1,22 @@ +import { + Dispatch as ReduxDispatch, + Middleware as ReduxMiddleware, + MiddlewareAPI as ReduxMiddlewareAPI, + Store as ReduxStore +} from 'redux' +import { ThunkAction as ReduxThunkAction } from 'redux-thunk' + +import { Action as _Action, State as _State } from '../reducer' + +export type Action = _Action +export type State = _State + +export interface Dispatch extends ReduxDispatch {} +export type GetState = () => State +export interface Middleware extends ReduxMiddleware { + (api: MiddlewareAPI): (next: Dispatch) => Dispatch +} +export interface MiddlewareAPI extends ReduxMiddlewareAPI {} +export interface Store extends ReduxStore {} +export interface ThunkAction extends ReduxThunkAction {} +export interface AsyncThunkAction extends ReduxThunkAction, State, any> {} diff --git a/server/src/utils/disconnect-all-hosts.ts b/server/src/utils/disconnect-all-hosts.ts new file mode 100644 index 0000000..906425d --- /dev/null +++ b/server/src/utils/disconnect-all-hosts.ts @@ -0,0 +1,26 @@ +import { thunks as hostThunks } from '../host' +import { Store } from '../types/redux' + +const DISCONNECT_REASON = 'shutdown' + +export const disconnectAllHosts = async (store: Store) => { + store.getState().hosts.forEach(host => store.dispatch(hostThunks.hostDisconnect(host.id, DISCONNECT_REASON))) + const allDisconnected = () => { + return store.getState().hosts + .map(host => ['disconnected', 'error'].includes(host.state.status)) + .reduce((acc, hostIsDown) => acc && hostIsDown, true) + } + + return new Promise((resolve) => { + let done = false + const listener = () => { + if (!done && allDisconnected()) { + done = true + resolve() + } + } + + listener() + store.subscribe(listener) + }) +} diff --git a/server/src/utils/error-with-code.ts b/server/src/utils/error-with-code.ts new file mode 100644 index 0000000..81f58ed --- /dev/null +++ b/server/src/utils/error-with-code.ts @@ -0,0 +1,8 @@ +export class ErrorWithCode extends Error { + code: number + constructor(code: number, message?: string) { + super(message) + this.name = 'ErrorWithCode' + this.code = code + } +} diff --git a/server/src/utils/graceful-shutdown.ts b/server/src/utils/graceful-shutdown.ts new file mode 100644 index 0000000..3e4f4da --- /dev/null +++ b/server/src/utils/graceful-shutdown.ts @@ -0,0 +1,31 @@ +import { createLogger } from './log' + +const log = createLogger(__filename) + +export const setupGracefulShutdown = (cleanup: () => Promise) => { + let exiting = false + const gracefulExit = async (code: number) => { + try { + if (exiting) { log.error('shutdown already requested'); return } + exiting = true + await cleanup() + } catch (err) { + log.error({ err }, 'error during shutdown') + } + + process.exit(code) + } + + process.on('unhandledRejection', (reason) => { + log.error('unhandled rejection:', reason) + gracefulExit(1) + }) + + process.on('uncaughtException', (err) => { + log.error({ err }, 'uncaught exception') + gracefulExit(1) + }) + + process.on('SIGTERM', () => { log.info('received SIGTERM'); gracefulExit(128 + 15) }) + process.on('SIGINT', () => { log.info('received SIGINT'); gracefulExit(128 + 2) }) +} diff --git a/server/src/utils/log-middleware.ts b/server/src/utils/log-middleware.ts new file mode 100644 index 0000000..f6b31ca --- /dev/null +++ b/server/src/utils/log-middleware.ts @@ -0,0 +1,12 @@ +import { createLogger } from './log' +import { Action, Dispatch, Middleware } from '../types/redux' + +const log = createLogger('redux') + +// @ts-ignore FIXME hpello https://github.com/gaearon/redux-thunk/issues/82 +export const logMiddleware: Middleware = () => (next: Dispatch) => (action: Action) => { + if (action.type !== 'SYSTEM_ADD_STATS') { + log.debug({ action }) + } + next(action) +} diff --git a/server/src/utils/log.ts b/server/src/utils/log.ts new file mode 100644 index 0000000..f5dff46 --- /dev/null +++ b/server/src/utils/log.ts @@ -0,0 +1,26 @@ +import { createLogger as bunyanCreateLogger, stdSerializers } from 'bunyan' +import { dirname, format, isAbsolute, relative, parse } from 'path' + +const log = bunyanCreateLogger({ + name: 'sshmon', + level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', + serializers: { err: stdSerializers.err } +}) + +// find basename of a file relative to root dir +const makeScope = (scopeOrFilename: string): string => { + if (!isAbsolute(scopeOrFilename)) { + return scopeOrFilename + } + + const rootDir = dirname(__dirname) + const relativeFilename = relative(rootDir, scopeOrFilename) + + const { dir, name } = parse(relativeFilename) + return format({ dir, name }) +} + +export const createLogger = (scopeOrFilename: string) => { + const scope = makeScope(scopeOrFilename) + return log.child({ scope }) +} diff --git a/server/src/utils/server-url.ts b/server/src/utils/server-url.ts new file mode 100644 index 0000000..8b72568 --- /dev/null +++ b/server/src/utils/server-url.ts @@ -0,0 +1,11 @@ +import { Server } from 'http' + +export const formatURL = (server: Server): string => { + const a = server.address() + if (typeof a === 'string') { return a } + + const { address, port, family } = a + + const host = family === 'IPv6' ? `[${address}]` : address + return `http://${host}:${port}` +} diff --git a/server/src/utils/tmp.ts b/server/src/utils/tmp.ts new file mode 100644 index 0000000..fdaa432 --- /dev/null +++ b/server/src/utils/tmp.ts @@ -0,0 +1,34 @@ +import { find } from 'lodash' +import { join } from 'path' +import { dir } from 'tmp' + +import { createLogger } from '../utils/log' + +const log = createLogger(__filename) + +const dirAsync = (options: any): Promise => new Promise((resolve, reject) => { + dir(options, (err, path) => { + if (err) { reject(err); return } + resolve(path) + }) +}) +const tmpDir = dirAsync({ prefix: 'sshmon-' }) +tmpDir.then(path => log.debug('temporary directory:', path)) + +const knownPaths: { scope: string, id: string, path: string }[] = [] + +// make short ids to ensure short unix paths +let numIds = 0 +const nextId = () => `${numIds++}` // tslint:disable-line no-increment-decrement + +export const makeTmpPath = (scopeOrFilename: string) => async (id: string): Promise => { + const scope = scopeOrFilename + const knownPath = find(knownPaths, { scope, id }) + const path = knownPath ? knownPath.path : nextId() + if (!knownPath) { + knownPaths.push({ scope, id, path }) + } + + const tmpDirPath = await tmpDir + return join(tmpDirPath, path) +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..1fe5226 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "lib": ["es5", "es6", "es7"] + }, + "include": [ + "src" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e569289 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "jsx": "react", + "lib": ["es2017", "dom"], + "target": "es5", + "sourceMap": true, + "strict": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true + } +} diff --git a/tslint.yml b/tslint.yml new file mode 100644 index 0000000..d6a2955 --- /dev/null +++ b/tslint.yml @@ -0,0 +1,31 @@ +defaultSeverity: error +extends: + - tslint-config-airbnb +rules: + align: + - true + # - arguments + - elements + - members + - parameters + - statements + import-name: false + max-line-length: false + no-console: true + no-trailing-whitespace: true + object-shorthand-properties-first: false + semicolon: + - true + - never + strict-boolean-expressions: false #// https://github.com/palantir/tslint/issues/3279 + ter-indent: + - true + - 2 + - SwitchCase: 0 + trailing-comma: + - true + - multiline: never + singleline: never + variable-name: + - true + - ban-keywords