├── plex-cluster-manager ├── .dockerignore ├── Dockerfile └── src │ ├── config.js │ ├── cron.js │ ├── listener.js │ ├── request.js │ ├── package.json │ ├── index.js │ ├── watcher.js │ ├── db.js │ ├── processor.js │ ├── populator.js │ └── package-lock.json ├── .gitignore ├── config └── plex-cluster-manager.sample.yml ├── plex-cluster-proxy ├── src │ ├── delivery.sh │ └── docker-entrypoint.sh ├── Dockerfile └── config │ └── nginx.conf ├── tools └── get-plex-token.sh ├── docker-compose.yml ├── nginx.conf └── README.md /plex-cluster-manager/.dockerignore: -------------------------------------------------------------------------------- 1 | src/node_modules 2 | src/plex.log 3 | src/plex-cluster.db -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | plex-cluster-manager/src/node_modules 2 | plex-cluster-manager/src/plex.log 3 | plex-cluster-manager/src/plex-cluster.db 4 | config/plex-cluster-manager.yml 5 | certs -------------------------------------------------------------------------------- /plex-cluster-manager/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | COPY --chown=node:node src /app 3 | WORKDIR /app 4 | RUN mkdir /data && chown node:node /data 5 | USER node 6 | RUN npm install 7 | CMD npm run prod 8 | -------------------------------------------------------------------------------- /config/plex-cluster-manager.sample.yml: -------------------------------------------------------------------------------- 1 | hosts: 2 | - host: plex.mydomain.com 3 | port: 11111 4 | token: xxxxxxxxxxxxxxxxxxxx 5 | #log: ./plex.log # This is not commonly used. It is only used to tail a local log. 6 | -------------------------------------------------------------------------------- /plex-cluster-proxy/src/delivery.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sleep 5; 4 | while [ -z "$(pgrep nginx)" ]; do 5 | echo "Waiting for nginx..."; 6 | sleep 5; 7 | done; 8 | 9 | tail -f /var/log/nginx/plex.log | while read -r LINE; do 10 | curl -s -X POST -H 'Content-Type: application/json' ${CLUSTER_MANAGER}?token=${CLUSTER_MANAGER_TOKEN} -d "${LINE}" > /dev/null; 11 | done 12 | -------------------------------------------------------------------------------- /plex-cluster-manager/src/config.js: -------------------------------------------------------------------------------- 1 | const yaml = require('js-yaml') 2 | const fs = require('fs') 3 | 4 | let _config = null; 5 | 6 | class Config { 7 | 8 | static async get() { 9 | //console.log("Db get"); 10 | if (!_config) 11 | _config = yaml.safeLoad(fs.readFileSync('/config/plex-cluster-manager.yml', 'utf8')); 12 | return _config; 13 | } 14 | 15 | } 16 | module.exports.Config = Config; 17 | -------------------------------------------------------------------------------- /plex-cluster-proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | # For debugging 3 | #FROM firesh/nginx-lua:alpine 4 | RUN \ 5 | mkdir -p /data &&\ 6 | touch /var/log/nginx/plex.log &&\ 7 | chown nginx:nginx /data /var/log/nginx/plex.log &&\ 8 | apk --no-cache add openssl curl bash 9 | COPY ./config/nginx.conf /etc/nginx/nginx.conf.template 10 | COPY ./src / 11 | ENTRYPOINT ["/docker-entrypoint.sh"] 12 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /plex-cluster-proxy/src/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eu 4 | 5 | if [ ! -f /data/dhparam.pem ]; then 6 | openssl dhparam -out /data/dhparam.pem 2048 7 | fi; 8 | 9 | envsubst '${PLEX_IP} ${PLEX_PORT} ${HTTPS_HOST} ${HTTPS_PORT} ${RESOLVERS} ${SSL_CERTIFICATE} ${SSL_CERTIFICATE_KEY}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf 10 | 11 | #nginx -g "daemon off;" & 12 | 13 | /delivery.sh & 14 | 15 | exec "$@" -------------------------------------------------------------------------------- /tools/get-plex-token.sh: -------------------------------------------------------------------------------- 1 | #/usr/bin/env bash 2 | 3 | read -p "Plex username: " USERNAME 4 | read -p "Plex password: " -s PASSWORD 5 | 6 | echo "" 7 | echo -n "Getting token from Plex..." 8 | 9 | TOKEN=$(curl -s -H 'X-Plex-Client-Identifier: plex-cluster' --data "user[login]=${USERNAME}" --data "user[password]=${PASSWORD}" 'https://plex.tv/users/sign_in.xml' \ 10 | | grep "" \ 11 | | sed 's///g;s/<\/authentication-token>//g') 12 | 13 | echo "" 14 | TOKEN=$(echo ${TOKEN}) 15 | echo "Your Plex token is: ${TOKEN}" -------------------------------------------------------------------------------- /plex-cluster-manager/src/cron.js: -------------------------------------------------------------------------------- 1 | const CronJob = require('cron').CronJob; 2 | const { Populator } = require('./populator') 3 | const { Processor } = require('./processor') 4 | const { Db } = require('./db'); 5 | 6 | class Cron { 7 | constructor() { 8 | } 9 | 10 | async start() { 11 | var job = new CronJob(`0 ${process.env.FULL_SYNC_SCHEDULE}`, async function () { 12 | console.log('Executing scheduled full sync'); 13 | const populator = new Populator(); 14 | await populator.update(true); 15 | const processor = new Processor(); 16 | await processor.syncAll(); 17 | }, null, true); 18 | job.start(); 19 | } 20 | 21 | } 22 | module.exports.Cron = Cron; 23 | -------------------------------------------------------------------------------- /plex-cluster-manager/src/listener.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const { Processor } = require('./processor'); 4 | 5 | module.exports = { 6 | 7 | start: function() { 8 | const app = express(); 9 | app.use(bodyParser.json()); 10 | const processor = new Processor(); 11 | 12 | app.post('/', (req, res) => { 13 | if (req.query.token && req.query.token == process.env.CLUSTER_MANAGER_TOKEN) { 14 | processor.parse(req.body); 15 | res.sendStatus(200); 16 | } else { 17 | console.log(`Invalid token supplied with ${JSON.stringify(req.body)}`); 18 | res.sendStatus(403); 19 | } 20 | }); 21 | 22 | app.listen(3400, () => console.log(`Listening on port 3400`)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /plex-cluster-manager/src/request.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const { Processor } = require('./processor'); 4 | 5 | module.exports = { 6 | 7 | start: function() { 8 | const app = express(); 9 | app.use(bodyParser.json()); 10 | const processor = new Processor(); 11 | 12 | app.post('/', (req, res) => { 13 | if (req.query.token && req.query.token == process.env.CLUSTER_MANAGER_TOKEN) { 14 | processor.parse(req.body); 15 | res.sendStatus(200); 16 | } else { 17 | console.log(`Invalid token supplied with ${JSON.stringify(req.body)}`); 18 | res.sendStatus(403); 19 | } 20 | }); 21 | 22 | app.listen(3400, () => console.log(`Listening on port 3400`)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /plex-cluster-manager/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plex-cluster", 3 | "version": "0.0.1", 4 | "description": "On-demand synchronization of Plex watched status across multiple servers", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon index.js", 8 | "prod": "node index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Fmstrat/plex-cluster.git" 13 | }, 14 | "author": "Fmstrat", 15 | "license": "GPL-3.0", 16 | "bugs": { 17 | "url": "https://github.com/Fmstrat/plex-cluster/issues" 18 | }, 19 | "homepage": "https://github.com/Fmstrat/plex-cluster", 20 | "devDependencies": { 21 | "nodemon": "^2.0.2" 22 | }, 23 | "dependencies": { 24 | "body-parser": "^1.19.0", 25 | "cron": "^1.8.2", 26 | "express": "^4.17.1", 27 | "js-yaml": "^3.13.1", 28 | "sqlite3": "^4.1.1", 29 | "superagent": "^5.2.1", 30 | "tail": "^2.0.3", 31 | "xml2js": "^0.4.23" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /plex-cluster-manager/src/index.js: -------------------------------------------------------------------------------- 1 | const { Watcher } = require('./watcher'); 2 | const { Cron } = require('./cron'); 3 | const { Db } = require('./db'); 4 | const listener = require('./listener'); 5 | const { Processor } = require('./processor'); 6 | const { Populator } = require('./populator'); 7 | 8 | async function main() { 9 | await Db.get(); 10 | await Db.build(); 11 | if (process.env.UPDATE_ON_START && process.env.UPDATE_ON_START == 'true') { 12 | const populator = new Populator(); 13 | await populator.update(true); 14 | const processor = new Processor(); 15 | await processor.syncAll(); 16 | } 17 | listener.start(); 18 | const watcher = new Watcher(); 19 | await watcher.start(); 20 | const cron = new Cron(); 21 | await cron.start(); 22 | return true; 23 | } 24 | 25 | process.on('exit', async function () { 26 | await Db.close(); 27 | console.log('exiting...'); 28 | }); 29 | 30 | main() 31 | .catch(err => console.error(err.stack)); 32 | 33 | -------------------------------------------------------------------------------- /plex-cluster-manager/src/watcher.js: -------------------------------------------------------------------------------- 1 | const Tail = require('tail').Tail; 2 | const { Processor } = require('./processor'); 3 | const { Config } = require('./config'); 4 | 5 | class Watcher { 6 | constructor() { 7 | } 8 | 9 | async start() { 10 | const config = await Config.get(); 11 | const hosts = config.hosts; 12 | for (var i = 0; i < hosts.length; i++) { 13 | if (hosts[i].log) { 14 | const processor = new Processor(); 15 | console.log(`Starting watcher for ${hosts[i].host}:${hosts[i].port}`) 16 | let tail = new Tail(hosts[i].log, { fromBeginning: true }); 17 | tail.on("line", function (line) { 18 | processor.parse(JSON.parse(line)); 19 | }); 20 | tail.on("error", function (error) { 21 | console.error('ERROR: ', error); 22 | }); 23 | 24 | } 25 | } 26 | } 27 | 28 | } 29 | module.exports.Watcher = Watcher; 30 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | 5 | # A proxy based on nginx that sits between the Plex server and 6 | # the internet. Every time a request is made to Plex, if that 7 | # request is to mark status, an API call is made 8 | # to plex-cluster-manager. 9 | # 10 | # There is one of these for every Plex server. 11 | plex-cluster-proxy: 12 | image: plex-cluster-proxy 13 | environment: 14 | - PLEX_IP=192.168.6.200 15 | - PLEX_PORT=32400 16 | - HTTPS_HOST=plex-northwyck.nowsci.com 17 | - HTTPS_PORT=33400 18 | - RESOLVERS=8.8.4.4 8.8.8.8 19 | - SSL_CERTIFICATE=/certs/fullchain.pem 20 | - SSL_CERTIFICATE_KEY=/certs/privkey.pem 21 | - CLUSTER_MANAGER=http://plex-cluster-manager:3400 22 | - CLUSTER_MANAGER_TOKEN=ts3wJgUNaJ6DcB7FhhYGcUpaa9DwPy 23 | volumes: 24 | - /etc/localtime:/etc/localtime:ro 25 | - ./certs/fullchain.pem:/certs/fullchain.pem:ro 26 | - ./certs/privkey.pem:/certs/privkey.pem:ro 27 | - plex-cluster-proxy:/data 28 | ports: 29 | - 33400:33400 30 | restart: always 31 | 32 | # The service that sychronizes the Plex servers. It takes the API calls 33 | # from plex-cluster-proxy, and reaches out to any other configured Plex 34 | # servers to set their status just as if the client connecting to the 35 | # original Plex server was connecting to that one instead. 36 | # 37 | # You will want to secure this behind an SSL proxy in the real world. 38 | # 39 | # There is only ever one of these. 40 | plex-cluster-manager: 41 | image: plex-cluster-manager 42 | command: 'npm run dev' 43 | environment: 44 | - CLUSTER_MANAGER_TOKEN=ts3wJgUNaJ6DcB7FhhYGcUpaa9DwPy 45 | volumes: 46 | - /etc/localtime:/etc/localtime:ro 47 | - ./config/plex-cluster-manager.yml:/config/plex-cluster-manager.yml:ro 48 | - ./plex-cluster-manager/src/:/app:z 49 | - /app/node_modules 50 | - plex-cluster-manager:/data 51 | restart: always 52 | 53 | volumes: 54 | plex-cluster-manager: 55 | plex-cluster-proxy: 56 | -------------------------------------------------------------------------------- /plex-cluster-manager/src/db.js: -------------------------------------------------------------------------------- 1 | const SQL = require('sqlite3') 2 | 3 | let _db = null; 4 | let _dbDebug = false; 5 | 6 | class Db { 7 | 8 | static async init() { 9 | //console.log("Db init"); 10 | if (!_db) { 11 | _db = new SQL.Database('/data/plex-cluster.db'); 12 | _db.getAsync = function (sql, params) { 13 | if (process.env.DEBUG && _dbDebug) console.log(`DEBUG: ${sql.replace(/\s\s+/g, ' ')}`); 14 | var that = this; 15 | return new Promise(function (resolve, reject) { 16 | that.get(sql, params, function (err, row) { 17 | if (err) { 18 | console.error(err); 19 | reject(err); 20 | } else 21 | resolve(row); 22 | }); 23 | }); 24 | }; 25 | _db.allAsync = function (sql, params) { 26 | if (process.env.DEBUG && _dbDebug) console.log(`DEBUG: ${sql.replace(/\s\s+/g, ' ')}`); 27 | var that = this; 28 | return new Promise(function (resolve, reject) { 29 | that.all(sql, params, function (err, rows) { 30 | if (err) { 31 | console.error(err); 32 | reject(err); 33 | } else 34 | resolve(rows); 35 | }); 36 | }); 37 | }; 38 | _db.runAsync = function (sql, params) { 39 | if (process.env.DEBUG && _dbDebug) console.log(`DEBUG: ${sql.replace(/\s\s+/g, ' ')}`); 40 | var that = this; 41 | return new Promise(function (resolve, reject) { 42 | that.run(sql, params, function (err, row) { 43 | if (err) { 44 | console.error(err); 45 | reject(err); 46 | } else 47 | resolve(row); 48 | }); 49 | }); 50 | }; 51 | } 52 | } 53 | 54 | static async build() { 55 | await _db.runAsync(` 56 | CREATE TABLE IF NOT EXISTS sections ( 57 | host TEXT NOT NULL, 58 | key INTEGER NOT NULL, 59 | type TEXT NOT NULL, 60 | title TEXT NOT NULL, 61 | UNIQUE ( 62 | host, 63 | key, 64 | type 65 | ) 66 | ON CONFLICT REPLACE 67 | ); 68 | `); 69 | await _db.runAsync(` 70 | CREATE TABLE IF NOT EXISTS media ( 71 | host TEXT NOT NULL, 72 | section_key INTEGER NOT NULL, 73 | guid TEXT NOT NULL, 74 | key TEXT NOT NULL, 75 | ratingKey INTEGER NOT NULL, 76 | title TEXT NOT NULL, 77 | UNIQUE ( 78 | host, 79 | section_key, 80 | guid 81 | ) 82 | ON CONFLICT REPLACE 83 | ); 84 | `); 85 | await _db.runAsync(` 86 | CREATE TABLE IF NOT EXISTS tokens ( 87 | host TEXT NOT NULL, 88 | token TEXT NOT NULL, 89 | clientIdentifier TEXT NOT NULL, 90 | username TEXT, 91 | updated_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 92 | UNIQUE ( 93 | host, 94 | token 95 | ) 96 | ON CONFLICT REPLACE 97 | ); 98 | `); 99 | await _db.runAsync(` 100 | CREATE TABLE IF NOT EXISTS viewCount ( 101 | host TEXT NOT NULL, 102 | username TEXT NOT NULL, 103 | section_key INTEGER NOT NULL, 104 | guid TEXT NOT NULL, 105 | key TEXT NOT NULL, 106 | ratingKey INTEGER NOT NULL, 107 | title TEXT NOT NULL, 108 | viewCount INTEGER NOT NULL, 109 | UNIQUE ( 110 | host, 111 | username, 112 | guid 113 | ) 114 | ON CONFLICT REPLACE 115 | ); 116 | `); 117 | } 118 | 119 | static async get() { 120 | //console.log("Db get"); 121 | if (!_db) 122 | await this.init(); 123 | return _db; 124 | } 125 | 126 | static async close() { 127 | //console.log("Db close"); 128 | if (!_db) 129 | _db.close(); 130 | } 131 | 132 | } 133 | module.exports.Db = Db; 134 | -------------------------------------------------------------------------------- /plex-cluster-proxy/config/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | 3 | events { 4 | worker_connections 768; 5 | } 6 | 7 | http { 8 | include /etc/nginx/mime.types; 9 | 10 | log_format json-log escape=json '{' 11 | '"remote_addr":"$remote_addr",' 12 | '"proxy_add_x_forwarded_for":"$proxy_add_x_forwarded_for",' 13 | '"host":"$http_host",' 14 | '"method":"$request_method",' 15 | '"uri":"$request_uri",' 16 | '"token":"$token",' 17 | '"identifier":"$identifier",' 18 | '"status":"$status"' 19 | '}'; 20 | # For debugging 21 | #log_format json-log escape=json '{' 22 | # '"remote_addr":"$remote_addr",' 23 | # '"proxy_add_x_forwarded_for":"$proxy_add_x_forwarded_for",' 24 | # '"request_headers":"$request_headers",' 25 | # '"host":"$http_host",' 26 | # '"method":"$request_method",' 27 | # '"uri":"$request_uri",' 28 | # '"token":"$token",' 29 | # '"identifier":"$identifier",' 30 | # '"status":"$status"' 31 | #'}'; 32 | 33 | # Upstream to Plex 34 | upstream plex_backend { 35 | server ${PLEX_IP}:${PLEX_PORT}; 36 | keepalive 32; 37 | } 38 | 39 | server { 40 | listen ${HTTPS_PORT} ssl http2; 41 | server_name ${HTTPS_HOST}; 42 | 43 | # For debugging 44 | #set_by_lua $request_headers ' 45 | # local h = ngx.req.get_headers() 46 | # local request_headers_all = "" 47 | # for k, v in pairs(h) do 48 | # request_headers_all = request_headers_all .. ""..k..": "..v..";" 49 | # end 50 | # return request_headers_all 51 | #'; 52 | 53 | send_timeout 100m; 54 | 55 | resolver ${RESOLVERS} valid=300s; 56 | resolver_timeout 10s; 57 | 58 | ssl_certificate ${SSL_CERTIFICATE}; 59 | ssl_certificate_key ${SSL_CERTIFICATE_KEY}; 60 | 61 | ssl_session_cache shared:SSL:10m; 62 | ssl_session_timeout 10m; 63 | 64 | ssl_protocols TLSv1.2; 65 | ssl_prefer_server_ciphers on; 66 | #Intentionally not hardened for security for player support and encryption video streams has a lot of overhead with something like AES-256-GCM-SHA384. 67 | ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; 68 | 69 | ssl_stapling on; 70 | ssl_stapling_verify on; 71 | 72 | #Turning this on will increase performance, but at the cost of security. Read below before making a choice. 73 | ssl_session_tickets off; 74 | 75 | ssl_dhparam /data/dhparam.pem; 76 | ssl_ecdh_curve secp384r1; 77 | 78 | gzip on; 79 | gzip_vary on; 80 | gzip_min_length 1000; 81 | gzip_proxied any; 82 | gzip_types text/plain text/css text/xml application/xml text/javascript application/x-javascript image/svg+xml; 83 | gzip_disable "MSIE [1-6]\."; 84 | 85 | client_max_body_size 100M; 86 | 87 | proxy_set_header Host $host; 88 | proxy_set_header X-Real-IP $remote_addr; 89 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 90 | proxy_set_header X-Forwarded-Proto $scheme; 91 | proxy_set_header Sec-WebSocket-Extensions $http_sec_websocket_extensions; 92 | proxy_set_header Sec-WebSocket-Key $http_sec_websocket_key; 93 | proxy_set_header Sec-WebSocket-Version $http_sec_websocket_version; 94 | 95 | proxy_http_version 1.1; 96 | proxy_set_header Upgrade $http_upgrade; 97 | proxy_set_header Connection "Upgrade"; 98 | 99 | proxy_redirect off; 100 | proxy_buffering off; 101 | 102 | #access_log /var/log/nginx/access.log; 103 | access_log off; 104 | 105 | set $pass_to_log 0; 106 | if ( $args !~ "plex-cluster=1" ) { 107 | set $pass_to_log 1; 108 | } 109 | 110 | set $token ""; 111 | if ( $http_x_plex_token ) { 112 | set $token $http_x_plex_token; 113 | } 114 | 115 | set $identifier ""; 116 | if ( $http_x_plex_client_identifier ) { 117 | set $identifier $http_x_plex_client_identifier; 118 | } 119 | 120 | location /:/scrobble { 121 | access_log /var/log/nginx/access.log json-log if=$pass_to_log; 122 | access_log /var/log/nginx/plex.log json-log if=$pass_to_log; 123 | proxy_pass https://plex_backend; 124 | } 125 | 126 | location /:/unscrobble { 127 | access_log /var/log/nginx/access.log json-log if=$pass_to_log; 128 | access_log /var/log/nginx/plex.log json-log if=$pass_to_log; 129 | proxy_pass https://plex_backend; 130 | } 131 | 132 | location /:/timeline { 133 | if ( $args !~ "state=stopped" ) { 134 | set $pass_to_log 0; 135 | } 136 | access_log /var/log/nginx/access.log json-log; 137 | access_log /var/log/nginx/plex.log json-log if=$pass_to_log; 138 | proxy_pass https://plex_backend; 139 | } 140 | 141 | location / { 142 | #access_log /var/log/nginx/access.log json-log; 143 | proxy_pass https://plex_backend; 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | events { 3 | worker_connections 768; 4 | } 5 | http { 6 | include /etc/nginx/mime.types; 7 | 8 | log_format json-log escape=json '{' 9 | '"host":"$http_host",' 10 | '"method":"$request_method",' 11 | '"uri":"$request_uri",' 12 | '"status":"$status"' 13 | '}'; 14 | 15 | #Upstream to Plex 16 | upstream plex_backend { 17 | #Set this to the IP address that appears in `ifconfig` (NATTED LAN IP or Public IP address) if you want the bandwidth meter in the server status page to work 18 | server 192.168.6.200:32400; 19 | keepalive 32; 20 | } 21 | server { 22 | #listen 80; 23 | #Enabling http2 can cause some issues with some devices, see #29 - Disable it if you experience issues 24 | listen 33400 ssl http2; #http2 can provide a substantial improvement for streaming: https://blog.cloudflare.com/introducing-http2/ 25 | server_name plex-northwyck.nowsci.com; 26 | 27 | send_timeout 100m; #Some players don't reopen a socket and playback stops totally instead of resuming after an extended pause (e.g. Chrome) 28 | 29 | #Faster resolving, improves stapling time. Timeout and nameservers may need to be adjusted for your location Google's have been used here. 30 | resolver 8.8.4.4 8.8.8.8 valid=300s; 31 | resolver_timeout 10s; 32 | 33 | #Use letsencrypt.org to get a free and trusted ssl certificate 34 | ssl_certificate /letsencrypt/live/nowsci.com/fullchain.pem; 35 | ssl_certificate_key /letsencrypt/live/nowsci.com/privkey.pem; 36 | 37 | ssl_session_cache shared:SSL:10m; 38 | ssl_session_timeout 10m; 39 | 40 | #ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 41 | ssl_protocols TLSv1.2; 42 | ssl_prefer_server_ciphers on; 43 | #Intentionally not hardened for security for player support and encryption video streams has a lot of overhead with something like AES-256-GCM-SHA384. 44 | ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; 45 | 46 | #Why this is important: https://blog.cloudflare.com/ocsp-stapling-how-cloudflare-just-made-ssl-30/ 47 | ssl_stapling on; 48 | ssl_stapling_verify on; 49 | #For letsencrypt.org you can get your chain like this: https://esham.io/2016/01/ocsp-stapling 50 | #ssl_trusted_certificate /path/to/chain.pem; 51 | 52 | #Reuse ssl sessions, avoids unnecessary handshakes 53 | #Turning this on will increase performance, but at the cost of security. Read below before making a choice. 54 | #https://github.com/mozilla/server-side-tls/issues/135 55 | #https://wiki.mozilla.org/Security/Server_Side_TLS#TLS_tickets_.28RFC_5077.29 56 | #ssl_session_tickets on; 57 | ssl_session_tickets off; 58 | 59 | #Use: openssl dhparam -out dhparam.pem 2048 - 4096 is better but for overhead reasons 2048 is enough for Plex. 60 | ssl_dhparam /dhparam.pem; 61 | ssl_ecdh_curve secp384r1; 62 | 63 | #Will ensure https is always used by supported browsers which prevents any server-side http > https redirects, as the browser will internally correct any request to https. 64 | #Recommended to submit to your domain to https://hstspreload.org as well. 65 | #!WARNING! Only enable this if you intend to only serve Plex over https, until this rule expires in your browser it WONT BE POSSIBLE to access Plex via http, remove 'includeSubDomains;' if you only want it to effect your Plex (sub-)domain. 66 | #This is disabled by default as it could cause issues with some playback devices it's advisable to test it with a small max-age and only enable if you don't encounter issues. (Haven't encountered any yet) 67 | #add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; 68 | 69 | #Plex has A LOT of javascript, xml and html. This helps a lot, but if it causes playback issues with devices turn it off. (Haven't encountered any yet) 70 | gzip on; 71 | gzip_vary on; 72 | gzip_min_length 1000; 73 | gzip_proxied any; 74 | gzip_types text/plain text/css text/xml application/xml text/javascript application/x-javascript image/svg+xml; 75 | gzip_disable "MSIE [1-6]\."; 76 | 77 | #Nginx default client_max_body_size is 1MB, which breaks Camera Upload feature from the phones. 78 | #Increasing the limit fixes the issue. Anyhow, if 4K videos are expected to be uploaded, the size might need to be increased even more 79 | client_max_body_size 100M; 80 | 81 | #Forward real ip and host to Plex 82 | proxy_set_header Host $host; 83 | proxy_set_header X-Real-IP $remote_addr; 84 | #When using ngx_http_realip_module change $proxy_add_x_forwarded_for to '$http_x_forwarded_for,$realip_remote_addr' 85 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 86 | proxy_set_header X-Forwarded-Proto $scheme; 87 | proxy_set_header Sec-WebSocket-Extensions $http_sec_websocket_extensions; 88 | proxy_set_header Sec-WebSocket-Key $http_sec_websocket_key; 89 | proxy_set_header Sec-WebSocket-Version $http_sec_websocket_version; 90 | 91 | #Websockets 92 | proxy_http_version 1.1; 93 | proxy_set_header Upgrade $http_upgrade; 94 | proxy_set_header Connection "Upgrade"; 95 | 96 | #Disables compression between Plex and Nginx, required if using sub_filter below. 97 | #May also improve loading time by a very marginal amount, as nginx will compress anyway. 98 | #proxy_set_header Accept-Encoding ""; 99 | 100 | #Buffering off send to the client as soon as the data is received from Plex. 101 | proxy_redirect off; 102 | proxy_buffering off; 103 | 104 | # Use json logging 105 | access_log /var/log/nginx/access.log; 106 | access_log /var/log/nginx/plex.log json-log; 107 | 108 | location / { 109 | #Example of using sub_filter to alter what Plex displays, this disables Plex News. 110 | #sub_filter ',news,' ','; 111 | #sub_filter_once on; 112 | #sub_filter_types text/xml; 113 | proxy_pass https://plex_backend; 114 | #proxy_pass http://192.168.6.200:32400; 115 | } 116 | 117 | #PlexPy forward example, works the same for other services. 118 | #location /plexpy { 119 | # proxy_pass http://127.0.0.1:8181; 120 | #} 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /plex-cluster-manager/src/processor.js: -------------------------------------------------------------------------------- 1 | const Request = require('superagent'); 2 | const URL = require('url'); 3 | const QueryString = require('querystring'); 4 | const { Db } = require('./db'); 5 | const { Config } = require('./config'); 6 | const { Populator } = require('./populator'); 7 | 8 | class Processor { 9 | constructor() { 10 | } 11 | 12 | async syncAll() { 13 | // Check for mismatched 0 vs > 0 14 | // Loop through results 15 | // Call api to other servers to update 16 | console.log(`Syncing watched status.`); 17 | const db = await Db.get(); 18 | const media = await db.allAsync(` 19 | SELECT DISTINCT v.*, 20 | ( 21 | SELECT t.token 22 | FROM tokens t 23 | WHERE t.username = v.username 24 | ORDER BY t.updated_ts DESC 25 | LIMIT 1 26 | ) AS token, 27 | v2.host AS hostGreater, 28 | v2.viewCount AS viewCountGreater 29 | FROM viewCount v, 30 | viewCount v2 31 | WHERE v.viewCount = 0 32 | AND v2.viewCount > 0 33 | AND v.guid = v2.guid 34 | AND v.host != v2.host 35 | AND ( 36 | SELECT title 37 | FROM sections 38 | WHERE key = v.section_key 39 | ) = ( 40 | SELECT title 41 | FROM sections 42 | WHERE key = v2.section_key 43 | ); 44 | `); 45 | for (var i = 0; i < media.length; i++) { 46 | console.log(`Syncing watched status for ${media[i].username}@${media[i].host} - ${media[i].title} (${media[i].guid})`); 47 | const url = `https://${media[i].host}/:/scrobble?identifier=com.plexapp.plugins.library&key=${media[i].ratingKey}&ratingKey=${media[i].ratingKey}&X-Plex-Token=${media[i].token}&plex-cluster=1`; 48 | try { 49 | const res = await Request.get(url); 50 | if (res.status == 200) { 51 | console.log(`Marked ${media[i].username}@${media[i].host} - ${media[i].title} (${media[i].guid}) watched.`); 52 | } else { 53 | console.error(`Error marking ${media[i].username}@${media[i].host} - ${media[i].title} (${media[i].guid}) watched.`); 54 | } 55 | } catch(err) { 56 | console.error(`ERROR: ${err.message}`); 57 | } 58 | } 59 | } 60 | 61 | async syncItem(data) { 62 | if (process.env.DEBUG) console.log(`DEBUG: ${data.uri}`); 63 | const uri = URL.parse(data.uri); 64 | const type = uri.pathname.slice(3, uri.pathname.length); 65 | let qs = QueryString.parse(uri.query); 66 | if (!qs['X-Plex-Token'] && data.token != '') 67 | qs['X-Plex-Token'] = data.token; 68 | if (!qs['X-Plex-Client-Identifier'] && data.identifier != '') 69 | qs['X-Plex-Client-Identifier'] = data.identifier; 70 | if (qs['X-Plex-Token']) { 71 | const db = await Db.get(); 72 | await db.runAsync(` 73 | INSERT INTO tokens ( 74 | host, 75 | token, 76 | clientIdentifier, 77 | updated_ts 78 | ) VALUES ( 79 | (?), 80 | (?), 81 | (?), 82 | CURRENT_TIMESTAMP 83 | ) 84 | `, [ 85 | data.host, 86 | qs['X-Plex-Token'], 87 | qs['X-Plex-Client-Identifier'] 88 | ]); 89 | var queryKey = qs.key; 90 | if (type == 'timeline') 91 | queryKey = qs.ratingKey; 92 | const srcMedia = await db.getAsync(` 93 | SELECT m.*, 94 | s.title as section_title 95 | FROM media m, 96 | sections s 97 | WHERE m.ratingKey = (?) 98 | AND m.host = (?) 99 | AND m.host = s.host 100 | AND m.section_key = s.key; 101 | `, [ 102 | queryKey, 103 | data.host 104 | ]); 105 | const config = await Config.get(); 106 | const hosts = config.hosts; 107 | if (srcMedia) { 108 | const config = await Config.get(); 109 | const hosts = config.hosts; 110 | let destFound = false; 111 | for (var i = 0; i < hosts.length; i++) { 112 | if (data.host != `${hosts[i].host}:${hosts[i].port}`) { 113 | destFound = true; 114 | const destMedia = await db.getAsync(` 115 | SELECT m.* 116 | FROM media m, 117 | sections s 118 | WHERE m.guid = (?) 119 | AND m.host = s.host 120 | AND m.section_key = s.key 121 | AND s.title = (?) 122 | AND m.host = (?); 123 | `, [ 124 | srcMedia.guid, 125 | srcMedia.section_title, 126 | `${hosts[i].host}:${hosts[i].port}` 127 | ]); 128 | if (!destMedia) { 129 | const populator = new Populator(); 130 | await populator.update(); 131 | } 132 | if (destMedia) { 133 | let marked = ''; 134 | if (type == 'timeline') { 135 | marked = 'stopped'; 136 | qs.key = destMedia.key; 137 | qs.ratingKey = destMedia.ratingKey; 138 | } else { 139 | if (type == 'scrobble') 140 | marked = 'watched'; 141 | else 142 | marked = 'unwatched'; 143 | qs.key = destMedia.ratingKey; 144 | } 145 | const url = `https://${hosts[i].host}:${hosts[i].port}${uri.pathname}?${QueryString.stringify(qs)}&plex-cluster=1`; 146 | try { 147 | const res = await Request.get(url); 148 | if (res.status == 200) { 149 | console.log(`Marked ${hosts[i].host}:${hosts[i].port} - ${destMedia.title} (${destMedia.guid}) ${marked}.`); 150 | } else { 151 | console.error(`Error marking ${hosts[i].host}:${hosts[i].port} - ${destMedia.title} (${destMedia.guid}) ${marked}.`); 152 | } 153 | } catch(err) { 154 | console.error(`ERROR: ${err.message}`); 155 | } 156 | } 157 | } 158 | } 159 | if (!destFound) 160 | console.error(`No destinations found other than source of ${hosts[i].host}:${hosts[i].port}.`); 161 | } else { 162 | console.error(`No source configured for ${hosts[i].host}:${hosts[i].port}.`); 163 | } 164 | } else { 165 | console.error(`ERROR: No token with request ${JSON.stringify(data)}`); 166 | } 167 | } 168 | 169 | parse(data) { 170 | if (data.status == '200') { 171 | this.syncItem(data); 172 | } else { 173 | console.error(`Not processing from ${data.host}${data.uri} when status is ${data.status}.`); 174 | } 175 | } 176 | 177 | } 178 | module.exports.Processor = Processor; 179 | -------------------------------------------------------------------------------- /plex-cluster-manager/src/populator.js: -------------------------------------------------------------------------------- 1 | const { Config } = require('./config'); 2 | const Request = require('superagent'); 3 | const parseString = require('xml2js').parseString; 4 | const { Db } = require('./db'); 5 | 6 | const size = 1000; 7 | 8 | let _running = false; 9 | let _skipPopulate = false; 10 | 11 | class Populator { 12 | constructor() { 13 | } 14 | 15 | async xml2json(xml) { 16 | return new Promise((resolve, reject) => { 17 | parseString(xml, function (err, json) { 18 | if (err) 19 | reject(err); 20 | else 21 | resolve(json); 22 | }); 23 | 24 | }); 25 | } 26 | 27 | async getPage(url, page, quiet) { 28 | const start = page * size; 29 | url += `&X-Plex-Container-Start=${start}&X-Plex-Container-Size=${size}`; 30 | if (!quiet) 31 | console.log(` Getting ${size} records starting at ${start}`); 32 | try { 33 | const res = await Request.get(url); 34 | if (res.status == 200) { 35 | const xml = await this.xml2json(res.text); 36 | return xml; 37 | } 38 | } catch(err) { 39 | console.error(`ERROR: ${err.message}`); 40 | } 41 | return null; 42 | } 43 | 44 | async getAll(url, type, quiet) { 45 | let data = []; 46 | let page = 0; 47 | while (true) { 48 | const xml = await this.getPage(url, page, quiet); 49 | data = data.concat(xml.MediaContainer[type]) 50 | if (xml.MediaContainer.$.size < size) 51 | break; 52 | page++; 53 | } 54 | return data; 55 | } 56 | 57 | async populateSections(host, viewCounts) { 58 | console.log(`Getting sections for ${host.host}:${host.port}`); 59 | const db = await Db.get(); 60 | const data = await this.getAll(`https://${host.host}:${host.port}/library/sections/?X-Plex-Token=${host.token}`, 'Directory'); 61 | console.log(`Retrieved ${data.length} records.`); 62 | for (var i = 0; i < data.length; i++) { 63 | const record = data[i].$; 64 | if ((record.type == 'movie' || record.type == 'show') && record.agent != 'com.plexapp.agents.none') { 65 | await db.runAsync(` 66 | INSERT OR REPLACE INTO sections ( 67 | host, 68 | key, 69 | type, 70 | title 71 | ) VALUES ( 72 | (?), 73 | (?), 74 | (?), 75 | (?) 76 | ) 77 | `, [ 78 | `${host.host}:${host.port}`, 79 | record.key, 80 | record.type, 81 | record.title 82 | ]); 83 | await this.populateMedia(host, record, viewCounts); 84 | } 85 | } 86 | } 87 | 88 | async populateMedia(host, section, viewCounts) { 89 | console.log(`Getting media for ${host.host}:${host.port} - ${section.title} (${section.key})`); 90 | const db = await Db.get(); 91 | let data = []; 92 | if (!_skipPopulate) 93 | data = await this.getAll(`https://${host.host}:${host.port}/library/sections/${section.key}/allLeaves?X-Plex-Token=${host.token}`, 'Video'); 94 | console.log(`Retrieved ${data.length} records.`); 95 | for (var i = 0; i < data.length; i++) { 96 | const record = data[i].$; 97 | if (!record.viewCount) { 98 | record.viewCount = 0; 99 | } 100 | await db.runAsync(` 101 | INSERT OR REPLACE INTO media ( 102 | host, 103 | section_key, 104 | guid, 105 | key, 106 | ratingKey, 107 | title 108 | ) VALUES ( 109 | (?), 110 | (?), 111 | (?), 112 | (?), 113 | (?), 114 | (?) 115 | ) 116 | `, [ 117 | `${host.host}:${host.port}`, 118 | section.key, 119 | record.guid, 120 | record.key, 121 | record.ratingKey, 122 | record.title, 123 | ]); 124 | } 125 | if (viewCounts) { 126 | // Get users 127 | const users = await db.allAsync(` 128 | SELECT username, 129 | token, 130 | clientIdentifier, 131 | max(updated_ts) as updated_ts 132 | FROM tokens 133 | WHERE username IS NOT NULL 134 | GROUP BY username; 135 | `); 136 | if (users) { 137 | // Loop through usernames 138 | for (var i = 0; i < users.length; i++) { 139 | await this.populateViewCounts(host, section, users[i]); 140 | } 141 | } 142 | } 143 | } 144 | 145 | async populateViewCounts(host, section, user) { 146 | // TODO: Expire old tokens 147 | // Query watched counts 148 | console.log(`Getting watched stats for ${user.username}@${host.host}:${host.port} - ${section.title} (${section.key})`); 149 | const db = await Db.get(); 150 | let data = []; 151 | if (!_skipPopulate) 152 | data = await this.getAll(`https://${host.host}:${host.port}/library/sections/${section.key}/allLeaves?X-Plex-Token=${user.token}`, 'Video'); 153 | console.log(`Retrieved ${data.length} records.`); 154 | for (var j = 0; j < data.length; j++) { 155 | const record = data[j].$; 156 | if (!record.viewCount) { 157 | record.viewCount = 0; 158 | } 159 | await db.runAsync(` 160 | INSERT OR REPLACE INTO viewCount ( 161 | host, 162 | username, 163 | section_key, 164 | guid, 165 | key, 166 | ratingKey, 167 | title, 168 | viewCount 169 | ) VALUES ( 170 | (?), 171 | (?), 172 | (?), 173 | (?), 174 | (?), 175 | (?), 176 | (?), 177 | (?) 178 | ) 179 | `, [ 180 | `${host.host}:${host.port}`, 181 | user.username, 182 | section.key, 183 | record.guid, 184 | record.key, 185 | record.ratingKey, 186 | record.title, 187 | record.viewCount 188 | ]); 189 | } 190 | } 191 | 192 | async updateUsernames() { 193 | console.log(`Updating username list from plex.tv.`); 194 | const db = await Db.get(); 195 | // Check for null usernames 196 | const nullUsers = await db.allAsync(` 197 | SELECT DISTINCT token 198 | FROM tokens 199 | WHERE username IS NULL; 200 | `); 201 | // Update null usernames from plex.tv 202 | for (var i = 0; i < nullUsers.length; i++) { 203 | const identifier = await db.getAsync(` 204 | SELECT clientIdentifier 205 | FROM tokens 206 | WHERE token = (?) 207 | ORDER BY updated_ts DESC 208 | LIMIT 1; 209 | `, [ 210 | nullUsers[i].token 211 | ]); 212 | if (identifier) { 213 | var url = `https://plex.tv/api/v2/user?X-Plex-Client-Identifier=${identifier.clientIdentifier}&X-Plex-Token=${nullUsers[i].token}`; 214 | console.log(url) 215 | try { 216 | const res = await Request.get(url) 217 | .buffer() 218 | .type('xml') 219 | .set('User-Agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0'); 220 | if (res.status == 200) { 221 | const xml = await this.xml2json(res.text); 222 | if (xml.user.$.username) { 223 | console.log(`Updated token ${nullUsers[i].token} with username ${xml.user.$.username}`); 224 | await db.runAsync(` 225 | UPDATE tokens 226 | SET username = (?) 227 | WHERE token = (?) 228 | AND username IS NULL; 229 | `, [ 230 | xml.user.$.username, 231 | nullUsers[i].token 232 | ]); 233 | } 234 | } 235 | 236 | } catch(err) { 237 | console.error(`ERROR: ${err.message}`); 238 | } 239 | } 240 | } 241 | } 242 | 243 | async sleep(ms) { 244 | return new Promise(resolve => setTimeout(resolve, ms)); 245 | } 246 | 247 | async update(viewCounts=false) { 248 | if (!_running) { 249 | _running = true; 250 | const config = await Config.get(); 251 | if (viewCounts) { 252 | await this.updateUsernames(); 253 | } 254 | for (var i = 0; i < config.hosts.length; i++) { 255 | const host = config.hosts[i]; 256 | await this.populateSections(host, viewCounts); 257 | } 258 | _running = false; 259 | } else { 260 | console.log('[*] A database update is already running, waiting for it to complete.') 261 | while (_running) 262 | await this.sleep(2000); 263 | console.log('[*] Update complete, continuing.') 264 | } 265 | } 266 | 267 | } 268 | module.exports.Populator = Populator; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plex Cluster 2 | Synchronizes the watched and timeline status between any number of Plex servers all using standard Plex APIs. 3 | 4 | ***This project is in BETA TESTING.*** 5 | 6 | Plex Cluster contains two applications: 7 | - **Plex Cluster Proxy**, which is installed alongside every Plex server and acts as a proxy (using `nginx`) between Plex Media Server and the internet. This application's job is to pass any requests that come to it along to the Plex server while catching any requests that mark watched status and also forwarding them on the **Plex Cluster Manager**. There is one instance of `Plex Cluster Proxy` for each Plex server. 8 | - **Plex Cluster Manager** is then used to synchronize the status between Plex servers. There is only one instance of `Plex Cluster Manager` which is used by all instances of `Plex Cluster Proxy`. 9 | 10 | An example use case: 11 | 1. Plex client requests to watch a show with GIUD (unique identifier) `ABC` from the `TV Shows` library on `Plex Server 1` 12 | 2. The request goes through `Plex Cluster Proxy 1` and is passed along to `Plex Server 1` 13 | 3. The user stops the show midway. 14 | 4. The request goes through `Plex Cluster Proxy 1` and is passed along to `Plex Server 1`, but is also passed along to `Plex Cluster Manager` 15 | 5. `Plex Cluster Manager` takes the request and checks for any other Plex server with a library named `TV Shows` and GUID `ABC` 16 | 6. For any other servers it finds, such as `Plex Server 2`, it forwards the request on to them and the show is instantly marked as watched up to that midway point. 17 | 7. User switches over to `Plex Server 2` and sees that they are midway through show `ABC` 18 | 19 | ## Features 20 | - Syncs watched status on-demand, right away 21 | - Uses standard Plex APIs without accessing the database 22 | - Can work across multiple Plex servers 23 | - Scheduled "full update" can completely sync user's watched status for any user that has been active on each Plex server since running `Plex Cluster` 24 | - Syncs media that is contained on both (or multiple) servers without erroring when media does not exist 25 | - Works for Plex.tv or Managed users 26 | 27 | ## To Do 28 | - Support "full sync" for users on some Roku models and other devices that encapsulate a temporary token in the request header (this does not impact on-demand sync) 29 | 30 | ## Requirements 31 | - Each Plex server must have a dedicated DNS or Dynamic DNS hostname 32 | 33 | ## Installation 34 | The best way to use `Plex Cluster` is with Docker. You can follow the `Dockerfile`s to set up the proxy manually, however if you're not running Docker for your services, you're missing out. 35 | 36 | This installation follows this example: 37 | - 2 Plex servers, one at home called `Home` and one remote called `Remote` 38 | - `Home` has... 39 | - a public DNS record of `plex-home.mydomain.com` 40 | - a Plex server running on `192.168.0.10:32400` 41 | - a Plex Cluster Proxy instance running on `https://plex-home.mydomain.com:32401` 42 | - a wildcard SSL certificate for `*.mydomain.com` in the `/certs` folder 43 | - `Remote` has... 44 | - a public DNS record of `plex-remote.mydomain.com` 45 | - a Plex server running on `192.168.1.10:32400` 46 | - a Plex Cluster Proxy instance running on `https://plex-remote.mydomain.com:32401` 47 | - a Plex Cluster Manager instance running on `https://plex-remote.mydomain.com:32402` 48 | - a wildcard SSL certificate for `*.mydomain.com` in the `/certs` folder 49 | 50 | ### Setting up the Home instance 51 | First, we need a Plex server, and a copy of `Plex Cluster Proxy` running at home. These can be spun up with the following `docker-compose.yml`: 52 | 53 | ``` yml 54 | version: '3.7' 55 | 56 | services: 57 | 58 | # A proxy based on nginx that sits between the Plex server and 59 | # the internet. Every time a request is made to Plex, if that 60 | # request is to mark status, an API call is made 61 | # to plex-cluster-manager. 62 | # 63 | # There is one of these for every Plex server. 64 | plex-cluster-proxy: 65 | image: nowsci/plex-cluster-proxy 66 | container_name: plex-cluster-proxy 67 | environment: 68 | - PLEX_IP=192.168.0.10 # The IP address of the Plex server's NIC 69 | - PLEX_PORT=32400 # The port Plex is listening on 70 | - HTTPS_HOST=plex-home.mydomain.com # The host that Plex Cluster Proxy will listen on 71 | - HTTPS_PORT=32401 # The port that Plex Cluster Proxy will listen on 72 | - RESOLVERS=8.8.4.4 8.8.8.8 # DNS servers that Plex Cluster Proxy should use 73 | - SSL_CERTIFICATE=/certs/fullchain.pem # Mapped location of the below SSL cert 74 | - SSL_CERTIFICATE_KEY=/certs/privkey.pem # Mapped location of the below SSL cert 75 | - CLUSTER_MANAGER=https://plex-remote.mydomain.com:32402 # URL of Plex Cluster Manager 76 | - CLUSTER_MANAGER_TOKEN=JgUNaJ6DcB7FhhYGcUpaa9DwPy # CHANGE THIS: A random token that Plex Cluster Proxy will use to authenticate with Manager 77 | volumes: 78 | - /etc/localtime:/etc/localtime:ro # Keeps time in sync 79 | - ./certs/fullchain.pem:/certs/fullchain.pem:ro # Mapped path to the SSL certificate 80 | - ./certs/privkey.pem:/certs/privkey.pem:ro # Mapped path to the SSL certificate 81 | - plex-cluster-proxy:/data # Docker volume where log data is stored for processing 82 | ports: 83 | - 32401:32401 # Port mapping (same as HTTPS_PORT), forward this port on your firewall 84 | restart: always # Restart on failure 85 | 86 | # The Home Plex server 87 | # This assumes you have Plex Pass 88 | plex: 89 | image: plexinc/pms-docker:plexpass 90 | container_name: plex 91 | environment: 92 | - TZ=America/New_York 93 | network_mode: bridge 94 | ports: 95 | - "192.168.0.10:32400:32400/tcp" # The port and IP to listen on, do not forward this port on your firewall 96 | volumes: 97 | - ./plex/plex/config:/config # Where you want your Plex Library/Config folder to be 98 | - /storage/transcode/plex:/transcode # A path for transcoding 99 | - /storage/transcode/plex/Sync+:/config/Library/Application Support/Plex Media Server/Cache/Transcode/Sync+ # To keep sync transcoding in the transcode folder 100 | - /storage/transcode/plex/Sync:/config/Library/Application Support/Plex Media Server/Cache/Transcode/Sync # To keep sync transcoding in the transcode folder 101 | - /storage/media:/media # Media folder 102 | restart: always 103 | 104 | volumes: 105 | plex-cluster-proxy: 106 | ``` 107 | 108 | After you spin this up with `docker-compose up -d`, navigate in your browser to `http://192.168.0.10:32400/web` and login to your new server. A few key things to set up are: 109 | - Settings -> Remote Access -> Disable Remote Access (Don't worry, you'll still be able to get here through your custom URL) 110 | - Settings -> Network -> Click Show Advanced 111 | - Settings -> Network -> Secure connections -> Choose `Required` 112 | - Settings -> Network -> Custom server access URLs -> Enter `https://plex-home.mydomain.com:32401` 113 | 114 | The home server setup is complete. 115 | 116 | ### Setting up the Remote instance 117 | Next we need a Plex server, a copy of `Plex Cluster Proxy` and a copy of `Plex Cluster Manager` running remotely. These can be spun up with the following `docker-compose.yml`: 118 | 119 | ``` yml 120 | version: '3.7' 121 | 122 | services: 123 | 124 | # A proxy based on nginx that sits between the Plex server and 125 | # the internet. Every time a request is made to Plex, if that 126 | # request is to mark status, an API call is made 127 | # to plex-cluster-manager. 128 | # 129 | # There is one of these for every Plex server. 130 | plex-cluster-proxy: 131 | image: nowsci/plex-cluster-proxy 132 | container_name: plex-cluster-proxy 133 | environment: 134 | - PLEX_IP=192.168.1.10 # The IP address of the Plex server's NIC 135 | - PLEX_PORT=32400 # The port Plex is listening on 136 | - HTTPS_HOST=plex-remote.mydomain.com # The host that Plex Cluster Proxy will listen on 137 | - HTTPS_PORT=32401 # The port that Plex Cluster Proxy will listen on 138 | - RESOLVERS=8.8.4.4 8.8.8.8 # DNS servers that Plex Cluster Proxy should use 139 | - SSL_CERTIFICATE=/certs/fullchain.pem # Mapped location of the below SSL cert 140 | - SSL_CERTIFICATE_KEY=/certs/privkey.pem # Mapped location of the below SSL cert 141 | - CLUSTER_MANAGER=http://plex-cluster-manager:32402 # URL of Plex Cluster Manager (local Docker URL in this case) 142 | - CLUSTER_MANAGER_TOKEN=JgUNaJ6DcB7FhhYGcUpaa9DwPy # CHANGE THIS: A random token that Plex Cluster Proxy will use to authenticate with Manager 143 | volumes: 144 | - /etc/localtime:/etc/localtime:ro # Keeps time in sync 145 | - ./certs/fullchain.pem:/certs/fullchain.pem:ro # Mapped path to the SSL certificate 146 | - ./certs/privkey.pem:/certs/privkey.pem:ro # Mapped path to the SSL certificate 147 | - plex-cluster-proxy:/data # Docker volume where log data is stored for processing 148 | ports: 149 | - 32401:32401 # Port mapping (same as HTTPS_PORT), forward this port on your firewall 150 | restart: always # Restart on failure 151 | 152 | # The service that sychronizes the Plex servers. It takes the API calls 153 | # from plex-cluster-proxy, and reaches out to any other configured Plex 154 | # servers to set their status just as if the client connecting to the 155 | # original Plex server was connecting to that one instead. 156 | # 157 | # You will want to secure this behind an SSL proxy in the real world. 158 | # 159 | # There is only ever one of these. 160 | plex-cluster-manager: 161 | image: nowsci/plex-cluster-manager 162 | container_name: plex-cluster-manager 163 | environment: 164 | - CLUSTER_MANAGER_TOKEN=JgUNaJ6DcB7FhhYGcUpaa9DwPy # CHANGE THIS: A random token that Plex Cluster Proxy will use to authenticate with Manager 165 | - UPDATE_ON_START=true # Do a full sync on start 166 | - DEBUG=false # Output debug logs 167 | - FULL_SYNC_SCHEDULE=0 2 * * * # A cron-styled schedule for when to run the full sync 168 | volumes: 169 | - /etc/localtime:/etc/localtime:ro # Keeps time in sync 170 | - ./config/plex-cluster-manager.yml:/config/plex-cluster-manager.yml:ro # A config file, see below 171 | - plex-cluster-manager:/data # Where Plex Cluster Manager stores it's SQLite DB 172 | restart: always 173 | 174 | # The Remote Plex server 175 | # This assumes you have Plex Pass 176 | # You could also clone the Home server and remove Preferences.xml before starting 177 | plex: 178 | image: plexinc/pms-docker:plexpass 179 | container_name: plex 180 | environment: 181 | - TZ=America/New_York 182 | network_mode: bridge 183 | ports: 184 | - "192.168.1.10:32400:32400/tcp" # The port and IP to listen on, do not forward this port on your firewall 185 | volumes: 186 | - ./plex/plex/config:/config # Where you want your Plex Library/Config folder to be 187 | - /storage/transcode/plex:/transcode # A path for transcoding 188 | - /storage/transcode/plex/Sync+:/config/Library/Application Support/Plex Media Server/Cache/Transcode/Sync+ # To keep sync transcoding in the transcode folder 189 | - /storage/transcode/plex/Sync:/config/Library/Application Support/Plex Media Server/Cache/Transcode/Sync # To keep sync transcoding in the transcode folder 190 | - /storage/media:/media # Media folder 191 | restart: always 192 | 193 | volumes: 194 | plex-cluster-manager: 195 | plex-cluster-proxy: 196 | ``` 197 | Before starting it up, you will need to create the `plex-cluster-manager.yml` configuration file ([sample](config/plex-cluster-manager.sample.yml)). This file looks like: 198 | ``` yml 199 | hosts: 200 | - host: plex.mydomain.com 201 | port: 11111 202 | token: xxxxxxxxxxxxxxxxxxxx 203 | #log: ./plex.log # This is not commonly used. It is only used to tail a local log. 204 | ``` 205 | The token should be a Plex token for the administrative user on the Plex server. You can get a token from Plex using [this script](tools/get-plex-token.sh). 206 | 207 | After you spin this up with `docker-compose up -d`, navigate in your browser to `http://192.168.1.10:32400/web` and login to your new server. A few key things to set up are: 208 | - Settings -> Remote Access -> Disable Remote Access (Don't worry, you'll still be able to get here through your custom URL) 209 | - Settings -> Network -> Click Show Advanced 210 | - Settings -> Network -> Secure connections -> Choose `Required` 211 | - Settings -> Network -> Custom server access URLs -> Enter `https://plex-remote.mydomain.com:32401` 212 | 213 | The remote server setup is complete. 214 | 215 | ### Final steps 216 | 217 | Once everything is complete, login via `https://plex.tv` and if you watch the logs via `docker-compose logs -ft` and mark shows watched you should start seeing `plex-cluster-proxy` and `plex-cluster-manager` synchronize the status. -------------------------------------------------------------------------------- /plex-cluster-manager/src/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plex-cluster", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "abbrev": { 8 | "version": "1.1.1", 9 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 10 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 11 | }, 12 | "accepts": { 13 | "version": "1.3.7", 14 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 15 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 16 | "requires": { 17 | "mime-types": "~2.1.24", 18 | "negotiator": "0.6.2" 19 | } 20 | }, 21 | "ajv": { 22 | "version": "6.11.0", 23 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", 24 | "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", 25 | "requires": { 26 | "fast-deep-equal": "^3.1.1", 27 | "fast-json-stable-stringify": "^2.0.0", 28 | "json-schema-traverse": "^0.4.1", 29 | "uri-js": "^4.2.2" 30 | } 31 | }, 32 | "ansi-align": { 33 | "version": "2.0.0", 34 | "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", 35 | "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", 36 | "dev": true, 37 | "requires": { 38 | "string-width": "^2.0.0" 39 | } 40 | }, 41 | "ansi-regex": { 42 | "version": "3.0.0", 43 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 44 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" 45 | }, 46 | "ansi-styles": { 47 | "version": "3.2.1", 48 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 49 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 50 | "dev": true, 51 | "requires": { 52 | "color-convert": "^1.9.0" 53 | } 54 | }, 55 | "anymatch": { 56 | "version": "3.1.1", 57 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", 58 | "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", 59 | "dev": true, 60 | "requires": { 61 | "normalize-path": "^3.0.0", 62 | "picomatch": "^2.0.4" 63 | } 64 | }, 65 | "aproba": { 66 | "version": "1.2.0", 67 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 68 | "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 69 | }, 70 | "are-we-there-yet": { 71 | "version": "1.1.5", 72 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", 73 | "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", 74 | "requires": { 75 | "delegates": "^1.0.0", 76 | "readable-stream": "^2.0.6" 77 | }, 78 | "dependencies": { 79 | "readable-stream": { 80 | "version": "2.3.7", 81 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 82 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 83 | "requires": { 84 | "core-util-is": "~1.0.0", 85 | "inherits": "~2.0.3", 86 | "isarray": "~1.0.0", 87 | "process-nextick-args": "~2.0.0", 88 | "safe-buffer": "~5.1.1", 89 | "string_decoder": "~1.1.1", 90 | "util-deprecate": "~1.0.1" 91 | } 92 | }, 93 | "safe-buffer": { 94 | "version": "5.1.2", 95 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 96 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 97 | }, 98 | "string_decoder": { 99 | "version": "1.1.1", 100 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 101 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 102 | "requires": { 103 | "safe-buffer": "~5.1.0" 104 | } 105 | } 106 | } 107 | }, 108 | "argparse": { 109 | "version": "1.0.10", 110 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 111 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 112 | "requires": { 113 | "sprintf-js": "~1.0.2" 114 | } 115 | }, 116 | "array-flatten": { 117 | "version": "1.1.1", 118 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 119 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 120 | }, 121 | "asn1": { 122 | "version": "0.2.4", 123 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 124 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 125 | "requires": { 126 | "safer-buffer": "~2.1.0" 127 | } 128 | }, 129 | "assert-plus": { 130 | "version": "1.0.0", 131 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 132 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 133 | }, 134 | "asynckit": { 135 | "version": "0.4.0", 136 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 137 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 138 | }, 139 | "aws-sign2": { 140 | "version": "0.7.0", 141 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 142 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 143 | }, 144 | "aws4": { 145 | "version": "1.9.1", 146 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", 147 | "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" 148 | }, 149 | "balanced-match": { 150 | "version": "1.0.0", 151 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 152 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 153 | }, 154 | "bcrypt-pbkdf": { 155 | "version": "1.0.2", 156 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 157 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 158 | "requires": { 159 | "tweetnacl": "^0.14.3" 160 | } 161 | }, 162 | "binary-extensions": { 163 | "version": "2.0.0", 164 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", 165 | "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", 166 | "dev": true 167 | }, 168 | "body-parser": { 169 | "version": "1.19.0", 170 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 171 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 172 | "requires": { 173 | "bytes": "3.1.0", 174 | "content-type": "~1.0.4", 175 | "debug": "2.6.9", 176 | "depd": "~1.1.2", 177 | "http-errors": "1.7.2", 178 | "iconv-lite": "0.4.24", 179 | "on-finished": "~2.3.0", 180 | "qs": "6.7.0", 181 | "raw-body": "2.4.0", 182 | "type-is": "~1.6.17" 183 | }, 184 | "dependencies": { 185 | "debug": { 186 | "version": "2.6.9", 187 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 188 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 189 | "requires": { 190 | "ms": "2.0.0" 191 | } 192 | }, 193 | "ms": { 194 | "version": "2.0.0", 195 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 196 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 197 | }, 198 | "qs": { 199 | "version": "6.7.0", 200 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 201 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 202 | } 203 | } 204 | }, 205 | "boxen": { 206 | "version": "1.3.0", 207 | "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", 208 | "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", 209 | "dev": true, 210 | "requires": { 211 | "ansi-align": "^2.0.0", 212 | "camelcase": "^4.0.0", 213 | "chalk": "^2.0.1", 214 | "cli-boxes": "^1.0.0", 215 | "string-width": "^2.0.0", 216 | "term-size": "^1.2.0", 217 | "widest-line": "^2.0.0" 218 | } 219 | }, 220 | "brace-expansion": { 221 | "version": "1.1.11", 222 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 223 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 224 | "requires": { 225 | "balanced-match": "^1.0.0", 226 | "concat-map": "0.0.1" 227 | } 228 | }, 229 | "braces": { 230 | "version": "3.0.2", 231 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 232 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 233 | "dev": true, 234 | "requires": { 235 | "fill-range": "^7.0.1" 236 | } 237 | }, 238 | "bytes": { 239 | "version": "3.1.0", 240 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 241 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 242 | }, 243 | "camelcase": { 244 | "version": "4.1.0", 245 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", 246 | "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", 247 | "dev": true 248 | }, 249 | "capture-stack-trace": { 250 | "version": "1.0.1", 251 | "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", 252 | "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", 253 | "dev": true 254 | }, 255 | "caseless": { 256 | "version": "0.12.0", 257 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 258 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 259 | }, 260 | "chalk": { 261 | "version": "2.4.2", 262 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 263 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 264 | "dev": true, 265 | "requires": { 266 | "ansi-styles": "^3.2.1", 267 | "escape-string-regexp": "^1.0.5", 268 | "supports-color": "^5.3.0" 269 | } 270 | }, 271 | "chokidar": { 272 | "version": "3.3.1", 273 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz", 274 | "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==", 275 | "dev": true, 276 | "requires": { 277 | "anymatch": "~3.1.1", 278 | "braces": "~3.0.2", 279 | "fsevents": "~2.1.2", 280 | "glob-parent": "~5.1.0", 281 | "is-binary-path": "~2.1.0", 282 | "is-glob": "~4.0.1", 283 | "normalize-path": "~3.0.0", 284 | "readdirp": "~3.3.0" 285 | } 286 | }, 287 | "chownr": { 288 | "version": "1.1.3", 289 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", 290 | "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==" 291 | }, 292 | "ci-info": { 293 | "version": "1.6.0", 294 | "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", 295 | "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", 296 | "dev": true 297 | }, 298 | "cli-boxes": { 299 | "version": "1.0.0", 300 | "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", 301 | "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", 302 | "dev": true 303 | }, 304 | "code-point-at": { 305 | "version": "1.1.0", 306 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 307 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 308 | }, 309 | "color-convert": { 310 | "version": "1.9.3", 311 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 312 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 313 | "dev": true, 314 | "requires": { 315 | "color-name": "1.1.3" 316 | } 317 | }, 318 | "color-name": { 319 | "version": "1.1.3", 320 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 321 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 322 | "dev": true 323 | }, 324 | "combined-stream": { 325 | "version": "1.0.8", 326 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 327 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 328 | "requires": { 329 | "delayed-stream": "~1.0.0" 330 | } 331 | }, 332 | "component-emitter": { 333 | "version": "1.3.0", 334 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", 335 | "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" 336 | }, 337 | "concat-map": { 338 | "version": "0.0.1", 339 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 340 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 341 | }, 342 | "configstore": { 343 | "version": "3.1.2", 344 | "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", 345 | "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", 346 | "dev": true, 347 | "requires": { 348 | "dot-prop": "^4.1.0", 349 | "graceful-fs": "^4.1.2", 350 | "make-dir": "^1.0.0", 351 | "unique-string": "^1.0.0", 352 | "write-file-atomic": "^2.0.0", 353 | "xdg-basedir": "^3.0.0" 354 | } 355 | }, 356 | "console-control-strings": { 357 | "version": "1.1.0", 358 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 359 | "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" 360 | }, 361 | "content-disposition": { 362 | "version": "0.5.3", 363 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 364 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 365 | "requires": { 366 | "safe-buffer": "5.1.2" 367 | }, 368 | "dependencies": { 369 | "safe-buffer": { 370 | "version": "5.1.2", 371 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 372 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 373 | } 374 | } 375 | }, 376 | "content-type": { 377 | "version": "1.0.4", 378 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 379 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 380 | }, 381 | "cookie": { 382 | "version": "0.4.0", 383 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 384 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 385 | }, 386 | "cookie-signature": { 387 | "version": "1.0.6", 388 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 389 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 390 | }, 391 | "cookiejar": { 392 | "version": "2.1.2", 393 | "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", 394 | "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" 395 | }, 396 | "core-util-is": { 397 | "version": "1.0.2", 398 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 399 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 400 | }, 401 | "create-error-class": { 402 | "version": "3.0.2", 403 | "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", 404 | "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", 405 | "dev": true, 406 | "requires": { 407 | "capture-stack-trace": "^1.0.0" 408 | } 409 | }, 410 | "cron": { 411 | "version": "1.8.2", 412 | "resolved": "https://registry.npmjs.org/cron/-/cron-1.8.2.tgz", 413 | "integrity": "sha512-Gk2c4y6xKEO8FSAUTklqtfSr7oTq0CiPQeLBG5Fl0qoXpZyMcj1SG59YL+hqq04bu6/IuEA7lMkYDAplQNKkyg==", 414 | "requires": { 415 | "moment-timezone": "^0.5.x" 416 | } 417 | }, 418 | "cross-spawn": { 419 | "version": "5.1.0", 420 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", 421 | "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", 422 | "dev": true, 423 | "requires": { 424 | "lru-cache": "^4.0.1", 425 | "shebang-command": "^1.2.0", 426 | "which": "^1.2.9" 427 | } 428 | }, 429 | "crypto-random-string": { 430 | "version": "1.0.0", 431 | "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", 432 | "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", 433 | "dev": true 434 | }, 435 | "dashdash": { 436 | "version": "1.14.1", 437 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 438 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 439 | "requires": { 440 | "assert-plus": "^1.0.0" 441 | } 442 | }, 443 | "debug": { 444 | "version": "3.2.6", 445 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 446 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 447 | "requires": { 448 | "ms": "^2.1.1" 449 | } 450 | }, 451 | "deep-extend": { 452 | "version": "0.6.0", 453 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 454 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 455 | }, 456 | "delayed-stream": { 457 | "version": "1.0.0", 458 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 459 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 460 | }, 461 | "delegates": { 462 | "version": "1.0.0", 463 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 464 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 465 | }, 466 | "depd": { 467 | "version": "1.1.2", 468 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 469 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 470 | }, 471 | "destroy": { 472 | "version": "1.0.4", 473 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 474 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 475 | }, 476 | "detect-libc": { 477 | "version": "1.0.3", 478 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", 479 | "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" 480 | }, 481 | "dot-prop": { 482 | "version": "4.2.1", 483 | "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", 484 | "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", 485 | "dev": true, 486 | "requires": { 487 | "is-obj": "^1.0.0" 488 | } 489 | }, 490 | "duplexer3": { 491 | "version": "0.1.4", 492 | "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", 493 | "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", 494 | "dev": true 495 | }, 496 | "ecc-jsbn": { 497 | "version": "0.1.2", 498 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 499 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 500 | "requires": { 501 | "jsbn": "~0.1.0", 502 | "safer-buffer": "^2.1.0" 503 | } 504 | }, 505 | "ee-first": { 506 | "version": "1.1.1", 507 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 508 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 509 | }, 510 | "encodeurl": { 511 | "version": "1.0.2", 512 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 513 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 514 | }, 515 | "escape-html": { 516 | "version": "1.0.3", 517 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 518 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 519 | }, 520 | "escape-string-regexp": { 521 | "version": "1.0.5", 522 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 523 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 524 | "dev": true 525 | }, 526 | "esprima": { 527 | "version": "4.0.1", 528 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 529 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" 530 | }, 531 | "etag": { 532 | "version": "1.8.1", 533 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 534 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 535 | }, 536 | "execa": { 537 | "version": "0.7.0", 538 | "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", 539 | "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", 540 | "dev": true, 541 | "requires": { 542 | "cross-spawn": "^5.0.1", 543 | "get-stream": "^3.0.0", 544 | "is-stream": "^1.1.0", 545 | "npm-run-path": "^2.0.0", 546 | "p-finally": "^1.0.0", 547 | "signal-exit": "^3.0.0", 548 | "strip-eof": "^1.0.0" 549 | } 550 | }, 551 | "express": { 552 | "version": "4.17.1", 553 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 554 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 555 | "requires": { 556 | "accepts": "~1.3.7", 557 | "array-flatten": "1.1.1", 558 | "body-parser": "1.19.0", 559 | "content-disposition": "0.5.3", 560 | "content-type": "~1.0.4", 561 | "cookie": "0.4.0", 562 | "cookie-signature": "1.0.6", 563 | "debug": "2.6.9", 564 | "depd": "~1.1.2", 565 | "encodeurl": "~1.0.2", 566 | "escape-html": "~1.0.3", 567 | "etag": "~1.8.1", 568 | "finalhandler": "~1.1.2", 569 | "fresh": "0.5.2", 570 | "merge-descriptors": "1.0.1", 571 | "methods": "~1.1.2", 572 | "on-finished": "~2.3.0", 573 | "parseurl": "~1.3.3", 574 | "path-to-regexp": "0.1.7", 575 | "proxy-addr": "~2.0.5", 576 | "qs": "6.7.0", 577 | "range-parser": "~1.2.1", 578 | "safe-buffer": "5.1.2", 579 | "send": "0.17.1", 580 | "serve-static": "1.14.1", 581 | "setprototypeof": "1.1.1", 582 | "statuses": "~1.5.0", 583 | "type-is": "~1.6.18", 584 | "utils-merge": "1.0.1", 585 | "vary": "~1.1.2" 586 | }, 587 | "dependencies": { 588 | "debug": { 589 | "version": "2.6.9", 590 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 591 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 592 | "requires": { 593 | "ms": "2.0.0" 594 | } 595 | }, 596 | "ms": { 597 | "version": "2.0.0", 598 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 599 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 600 | }, 601 | "qs": { 602 | "version": "6.7.0", 603 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 604 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 605 | }, 606 | "safe-buffer": { 607 | "version": "5.1.2", 608 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 609 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 610 | } 611 | } 612 | }, 613 | "extend": { 614 | "version": "3.0.2", 615 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 616 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 617 | }, 618 | "extsprintf": { 619 | "version": "1.3.0", 620 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 621 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 622 | }, 623 | "fast-deep-equal": { 624 | "version": "3.1.1", 625 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", 626 | "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" 627 | }, 628 | "fast-json-stable-stringify": { 629 | "version": "2.1.0", 630 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 631 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 632 | }, 633 | "fast-safe-stringify": { 634 | "version": "2.0.7", 635 | "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", 636 | "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" 637 | }, 638 | "fill-range": { 639 | "version": "7.0.1", 640 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 641 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 642 | "dev": true, 643 | "requires": { 644 | "to-regex-range": "^5.0.1" 645 | } 646 | }, 647 | "finalhandler": { 648 | "version": "1.1.2", 649 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 650 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 651 | "requires": { 652 | "debug": "2.6.9", 653 | "encodeurl": "~1.0.2", 654 | "escape-html": "~1.0.3", 655 | "on-finished": "~2.3.0", 656 | "parseurl": "~1.3.3", 657 | "statuses": "~1.5.0", 658 | "unpipe": "~1.0.0" 659 | }, 660 | "dependencies": { 661 | "debug": { 662 | "version": "2.6.9", 663 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 664 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 665 | "requires": { 666 | "ms": "2.0.0" 667 | } 668 | }, 669 | "ms": { 670 | "version": "2.0.0", 671 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 672 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 673 | } 674 | } 675 | }, 676 | "forever-agent": { 677 | "version": "0.6.1", 678 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 679 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 680 | }, 681 | "form-data": { 682 | "version": "3.0.0", 683 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", 684 | "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", 685 | "requires": { 686 | "asynckit": "^0.4.0", 687 | "combined-stream": "^1.0.8", 688 | "mime-types": "^2.1.12" 689 | } 690 | }, 691 | "formidable": { 692 | "version": "1.2.1", 693 | "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", 694 | "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==" 695 | }, 696 | "forwarded": { 697 | "version": "0.1.2", 698 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 699 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 700 | }, 701 | "fresh": { 702 | "version": "0.5.2", 703 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 704 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 705 | }, 706 | "fs-minipass": { 707 | "version": "1.2.7", 708 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", 709 | "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", 710 | "requires": { 711 | "minipass": "^2.6.0" 712 | } 713 | }, 714 | "fs.realpath": { 715 | "version": "1.0.0", 716 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 717 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 718 | }, 719 | "fsevents": { 720 | "version": "2.1.2", 721 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", 722 | "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", 723 | "dev": true, 724 | "optional": true 725 | }, 726 | "gauge": { 727 | "version": "2.7.4", 728 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", 729 | "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", 730 | "requires": { 731 | "aproba": "^1.0.3", 732 | "console-control-strings": "^1.0.0", 733 | "has-unicode": "^2.0.0", 734 | "object-assign": "^4.1.0", 735 | "signal-exit": "^3.0.0", 736 | "string-width": "^1.0.1", 737 | "strip-ansi": "^3.0.1", 738 | "wide-align": "^1.1.0" 739 | }, 740 | "dependencies": { 741 | "ansi-regex": { 742 | "version": "2.1.1", 743 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 744 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 745 | }, 746 | "is-fullwidth-code-point": { 747 | "version": "1.0.0", 748 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 749 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 750 | "requires": { 751 | "number-is-nan": "^1.0.0" 752 | } 753 | }, 754 | "string-width": { 755 | "version": "1.0.2", 756 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 757 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 758 | "requires": { 759 | "code-point-at": "^1.0.0", 760 | "is-fullwidth-code-point": "^1.0.0", 761 | "strip-ansi": "^3.0.0" 762 | } 763 | }, 764 | "strip-ansi": { 765 | "version": "3.0.1", 766 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 767 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 768 | "requires": { 769 | "ansi-regex": "^2.0.0" 770 | } 771 | } 772 | } 773 | }, 774 | "get-stream": { 775 | "version": "3.0.0", 776 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", 777 | "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", 778 | "dev": true 779 | }, 780 | "getpass": { 781 | "version": "0.1.7", 782 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 783 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 784 | "requires": { 785 | "assert-plus": "^1.0.0" 786 | } 787 | }, 788 | "glob": { 789 | "version": "7.1.6", 790 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 791 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 792 | "requires": { 793 | "fs.realpath": "^1.0.0", 794 | "inflight": "^1.0.4", 795 | "inherits": "2", 796 | "minimatch": "^3.0.4", 797 | "once": "^1.3.0", 798 | "path-is-absolute": "^1.0.0" 799 | } 800 | }, 801 | "glob-parent": { 802 | "version": "5.1.0", 803 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", 804 | "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", 805 | "dev": true, 806 | "requires": { 807 | "is-glob": "^4.0.1" 808 | } 809 | }, 810 | "global-dirs": { 811 | "version": "0.1.1", 812 | "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", 813 | "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", 814 | "dev": true, 815 | "requires": { 816 | "ini": "^1.3.4" 817 | } 818 | }, 819 | "got": { 820 | "version": "6.7.1", 821 | "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", 822 | "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", 823 | "dev": true, 824 | "requires": { 825 | "create-error-class": "^3.0.0", 826 | "duplexer3": "^0.1.4", 827 | "get-stream": "^3.0.0", 828 | "is-redirect": "^1.0.0", 829 | "is-retry-allowed": "^1.0.0", 830 | "is-stream": "^1.0.0", 831 | "lowercase-keys": "^1.0.0", 832 | "safe-buffer": "^5.0.1", 833 | "timed-out": "^4.0.0", 834 | "unzip-response": "^2.0.1", 835 | "url-parse-lax": "^1.0.0" 836 | } 837 | }, 838 | "graceful-fs": { 839 | "version": "4.2.3", 840 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", 841 | "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", 842 | "dev": true 843 | }, 844 | "har-schema": { 845 | "version": "2.0.0", 846 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 847 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 848 | }, 849 | "har-validator": { 850 | "version": "5.1.3", 851 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 852 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 853 | "requires": { 854 | "ajv": "^6.5.5", 855 | "har-schema": "^2.0.0" 856 | } 857 | }, 858 | "has-flag": { 859 | "version": "3.0.0", 860 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 861 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 862 | "dev": true 863 | }, 864 | "has-unicode": { 865 | "version": "2.0.1", 866 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 867 | "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" 868 | }, 869 | "http-errors": { 870 | "version": "1.7.2", 871 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 872 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 873 | "requires": { 874 | "depd": "~1.1.2", 875 | "inherits": "2.0.3", 876 | "setprototypeof": "1.1.1", 877 | "statuses": ">= 1.5.0 < 2", 878 | "toidentifier": "1.0.0" 879 | }, 880 | "dependencies": { 881 | "inherits": { 882 | "version": "2.0.3", 883 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 884 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 885 | } 886 | } 887 | }, 888 | "http-signature": { 889 | "version": "1.2.0", 890 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 891 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 892 | "requires": { 893 | "assert-plus": "^1.0.0", 894 | "jsprim": "^1.2.2", 895 | "sshpk": "^1.7.0" 896 | } 897 | }, 898 | "iconv-lite": { 899 | "version": "0.4.24", 900 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 901 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 902 | "requires": { 903 | "safer-buffer": ">= 2.1.2 < 3" 904 | } 905 | }, 906 | "ignore-by-default": { 907 | "version": "1.0.1", 908 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", 909 | "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", 910 | "dev": true 911 | }, 912 | "ignore-walk": { 913 | "version": "3.0.3", 914 | "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", 915 | "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", 916 | "requires": { 917 | "minimatch": "^3.0.4" 918 | } 919 | }, 920 | "import-lazy": { 921 | "version": "2.1.0", 922 | "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", 923 | "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", 924 | "dev": true 925 | }, 926 | "imurmurhash": { 927 | "version": "0.1.4", 928 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 929 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 930 | "dev": true 931 | }, 932 | "inflight": { 933 | "version": "1.0.6", 934 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 935 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 936 | "requires": { 937 | "once": "^1.3.0", 938 | "wrappy": "1" 939 | } 940 | }, 941 | "inherits": { 942 | "version": "2.0.4", 943 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 944 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 945 | }, 946 | "ini": { 947 | "version": "1.3.8", 948 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 949 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" 950 | }, 951 | "ipaddr.js": { 952 | "version": "1.9.0", 953 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", 954 | "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" 955 | }, 956 | "is-binary-path": { 957 | "version": "2.1.0", 958 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 959 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 960 | "dev": true, 961 | "requires": { 962 | "binary-extensions": "^2.0.0" 963 | } 964 | }, 965 | "is-ci": { 966 | "version": "1.2.1", 967 | "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", 968 | "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", 969 | "dev": true, 970 | "requires": { 971 | "ci-info": "^1.5.0" 972 | } 973 | }, 974 | "is-extglob": { 975 | "version": "2.1.1", 976 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 977 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 978 | "dev": true 979 | }, 980 | "is-fullwidth-code-point": { 981 | "version": "2.0.0", 982 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 983 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" 984 | }, 985 | "is-glob": { 986 | "version": "4.0.1", 987 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", 988 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", 989 | "dev": true, 990 | "requires": { 991 | "is-extglob": "^2.1.1" 992 | } 993 | }, 994 | "is-installed-globally": { 995 | "version": "0.1.0", 996 | "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", 997 | "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", 998 | "dev": true, 999 | "requires": { 1000 | "global-dirs": "^0.1.0", 1001 | "is-path-inside": "^1.0.0" 1002 | } 1003 | }, 1004 | "is-npm": { 1005 | "version": "1.0.0", 1006 | "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", 1007 | "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", 1008 | "dev": true 1009 | }, 1010 | "is-number": { 1011 | "version": "7.0.0", 1012 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1013 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1014 | "dev": true 1015 | }, 1016 | "is-obj": { 1017 | "version": "1.0.1", 1018 | "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", 1019 | "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", 1020 | "dev": true 1021 | }, 1022 | "is-path-inside": { 1023 | "version": "1.0.1", 1024 | "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", 1025 | "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", 1026 | "dev": true, 1027 | "requires": { 1028 | "path-is-inside": "^1.0.1" 1029 | } 1030 | }, 1031 | "is-redirect": { 1032 | "version": "1.0.0", 1033 | "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", 1034 | "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", 1035 | "dev": true 1036 | }, 1037 | "is-retry-allowed": { 1038 | "version": "1.2.0", 1039 | "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", 1040 | "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", 1041 | "dev": true 1042 | }, 1043 | "is-stream": { 1044 | "version": "1.1.0", 1045 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 1046 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", 1047 | "dev": true 1048 | }, 1049 | "is-typedarray": { 1050 | "version": "1.0.0", 1051 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 1052 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 1053 | }, 1054 | "isarray": { 1055 | "version": "1.0.0", 1056 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 1057 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 1058 | }, 1059 | "isexe": { 1060 | "version": "2.0.0", 1061 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1062 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 1063 | "dev": true 1064 | }, 1065 | "isstream": { 1066 | "version": "0.1.2", 1067 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 1068 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 1069 | }, 1070 | "js-yaml": { 1071 | "version": "3.13.1", 1072 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", 1073 | "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", 1074 | "requires": { 1075 | "argparse": "^1.0.7", 1076 | "esprima": "^4.0.0" 1077 | } 1078 | }, 1079 | "jsbn": { 1080 | "version": "0.1.1", 1081 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 1082 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 1083 | }, 1084 | "json-schema": { 1085 | "version": "0.2.3", 1086 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 1087 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 1088 | }, 1089 | "json-schema-traverse": { 1090 | "version": "0.4.1", 1091 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 1092 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 1093 | }, 1094 | "json-stringify-safe": { 1095 | "version": "5.0.1", 1096 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 1097 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 1098 | }, 1099 | "jsprim": { 1100 | "version": "1.4.1", 1101 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 1102 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 1103 | "requires": { 1104 | "assert-plus": "1.0.0", 1105 | "extsprintf": "1.3.0", 1106 | "json-schema": "0.2.3", 1107 | "verror": "1.10.0" 1108 | } 1109 | }, 1110 | "latest-version": { 1111 | "version": "3.1.0", 1112 | "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", 1113 | "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", 1114 | "dev": true, 1115 | "requires": { 1116 | "package-json": "^4.0.0" 1117 | } 1118 | }, 1119 | "lowercase-keys": { 1120 | "version": "1.0.1", 1121 | "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", 1122 | "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", 1123 | "dev": true 1124 | }, 1125 | "lru-cache": { 1126 | "version": "4.1.5", 1127 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", 1128 | "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", 1129 | "dev": true, 1130 | "requires": { 1131 | "pseudomap": "^1.0.2", 1132 | "yallist": "^2.1.2" 1133 | } 1134 | }, 1135 | "make-dir": { 1136 | "version": "1.3.0", 1137 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", 1138 | "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", 1139 | "dev": true, 1140 | "requires": { 1141 | "pify": "^3.0.0" 1142 | } 1143 | }, 1144 | "media-typer": { 1145 | "version": "0.3.0", 1146 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 1147 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 1148 | }, 1149 | "merge-descriptors": { 1150 | "version": "1.0.1", 1151 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 1152 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 1153 | }, 1154 | "methods": { 1155 | "version": "1.1.2", 1156 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 1157 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 1158 | }, 1159 | "mime": { 1160 | "version": "2.4.4", 1161 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", 1162 | "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" 1163 | }, 1164 | "mime-db": { 1165 | "version": "1.43.0", 1166 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", 1167 | "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" 1168 | }, 1169 | "mime-types": { 1170 | "version": "2.1.26", 1171 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", 1172 | "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", 1173 | "requires": { 1174 | "mime-db": "1.43.0" 1175 | } 1176 | }, 1177 | "minimatch": { 1178 | "version": "3.0.4", 1179 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 1180 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 1181 | "requires": { 1182 | "brace-expansion": "^1.1.7" 1183 | } 1184 | }, 1185 | "minimist": { 1186 | "version": "1.2.0", 1187 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 1188 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" 1189 | }, 1190 | "minipass": { 1191 | "version": "2.9.0", 1192 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", 1193 | "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", 1194 | "requires": { 1195 | "safe-buffer": "^5.1.2", 1196 | "yallist": "^3.0.0" 1197 | }, 1198 | "dependencies": { 1199 | "yallist": { 1200 | "version": "3.1.1", 1201 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 1202 | "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" 1203 | } 1204 | } 1205 | }, 1206 | "minizlib": { 1207 | "version": "1.3.3", 1208 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", 1209 | "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", 1210 | "requires": { 1211 | "minipass": "^2.9.0" 1212 | } 1213 | }, 1214 | "mkdirp": { 1215 | "version": "0.5.1", 1216 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 1217 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 1218 | "requires": { 1219 | "minimist": "0.0.8" 1220 | }, 1221 | "dependencies": { 1222 | "minimist": { 1223 | "version": "0.0.8", 1224 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 1225 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 1226 | } 1227 | } 1228 | }, 1229 | "moment": { 1230 | "version": "2.24.0", 1231 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", 1232 | "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" 1233 | }, 1234 | "moment-timezone": { 1235 | "version": "0.5.27", 1236 | "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.27.tgz", 1237 | "integrity": "sha512-EIKQs7h5sAsjhPCqN6ggx6cEbs94GK050254TIJySD1bzoM5JTYDwAU1IoVOeTOL6Gm27kYJ51/uuvq1kIlrbw==", 1238 | "requires": { 1239 | "moment": ">= 2.9.0" 1240 | } 1241 | }, 1242 | "ms": { 1243 | "version": "2.1.2", 1244 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1245 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 1246 | }, 1247 | "nan": { 1248 | "version": "2.14.0", 1249 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", 1250 | "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" 1251 | }, 1252 | "needle": { 1253 | "version": "2.4.0", 1254 | "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", 1255 | "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", 1256 | "requires": { 1257 | "debug": "^3.2.6", 1258 | "iconv-lite": "^0.4.4", 1259 | "sax": "^1.2.4" 1260 | } 1261 | }, 1262 | "negotiator": { 1263 | "version": "0.6.2", 1264 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 1265 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 1266 | }, 1267 | "node-pre-gyp": { 1268 | "version": "0.11.0", 1269 | "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", 1270 | "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", 1271 | "requires": { 1272 | "detect-libc": "^1.0.2", 1273 | "mkdirp": "^0.5.1", 1274 | "needle": "^2.2.1", 1275 | "nopt": "^4.0.1", 1276 | "npm-packlist": "^1.1.6", 1277 | "npmlog": "^4.0.2", 1278 | "rc": "^1.2.7", 1279 | "rimraf": "^2.6.1", 1280 | "semver": "^5.3.0", 1281 | "tar": "^4" 1282 | }, 1283 | "dependencies": { 1284 | "nopt": { 1285 | "version": "4.0.1", 1286 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", 1287 | "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", 1288 | "requires": { 1289 | "abbrev": "1", 1290 | "osenv": "^0.1.4" 1291 | } 1292 | } 1293 | } 1294 | }, 1295 | "nodemon": { 1296 | "version": "2.0.2", 1297 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.2.tgz", 1298 | "integrity": "sha512-GWhYPMfde2+M0FsHnggIHXTqPDHXia32HRhh6H0d75Mt9FKUoCBvumNHr7LdrpPBTKxsWmIEOjoN+P4IU6Hcaw==", 1299 | "dev": true, 1300 | "requires": { 1301 | "chokidar": "^3.2.2", 1302 | "debug": "^3.2.6", 1303 | "ignore-by-default": "^1.0.1", 1304 | "minimatch": "^3.0.4", 1305 | "pstree.remy": "^1.1.7", 1306 | "semver": "^5.7.1", 1307 | "supports-color": "^5.5.0", 1308 | "touch": "^3.1.0", 1309 | "undefsafe": "^2.0.2", 1310 | "update-notifier": "^2.5.0" 1311 | } 1312 | }, 1313 | "nopt": { 1314 | "version": "1.0.10", 1315 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", 1316 | "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", 1317 | "dev": true, 1318 | "requires": { 1319 | "abbrev": "1" 1320 | } 1321 | }, 1322 | "normalize-path": { 1323 | "version": "3.0.0", 1324 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1325 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1326 | "dev": true 1327 | }, 1328 | "npm-bundled": { 1329 | "version": "1.1.1", 1330 | "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", 1331 | "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", 1332 | "requires": { 1333 | "npm-normalize-package-bin": "^1.0.1" 1334 | } 1335 | }, 1336 | "npm-normalize-package-bin": { 1337 | "version": "1.0.1", 1338 | "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", 1339 | "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" 1340 | }, 1341 | "npm-packlist": { 1342 | "version": "1.4.7", 1343 | "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.7.tgz", 1344 | "integrity": "sha512-vAj7dIkp5NhieaGZxBJB8fF4R0078rqsmhJcAfXZ6O7JJhjhPK96n5Ry1oZcfLXgfun0GWTZPOxaEyqv8GBykQ==", 1345 | "requires": { 1346 | "ignore-walk": "^3.0.1", 1347 | "npm-bundled": "^1.0.1" 1348 | } 1349 | }, 1350 | "npm-run-path": { 1351 | "version": "2.0.2", 1352 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", 1353 | "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", 1354 | "dev": true, 1355 | "requires": { 1356 | "path-key": "^2.0.0" 1357 | } 1358 | }, 1359 | "npmlog": { 1360 | "version": "4.1.2", 1361 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", 1362 | "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", 1363 | "requires": { 1364 | "are-we-there-yet": "~1.1.2", 1365 | "console-control-strings": "~1.1.0", 1366 | "gauge": "~2.7.3", 1367 | "set-blocking": "~2.0.0" 1368 | } 1369 | }, 1370 | "number-is-nan": { 1371 | "version": "1.0.1", 1372 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 1373 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 1374 | }, 1375 | "oauth-sign": { 1376 | "version": "0.9.0", 1377 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 1378 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 1379 | }, 1380 | "object-assign": { 1381 | "version": "4.1.1", 1382 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1383 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 1384 | }, 1385 | "on-finished": { 1386 | "version": "2.3.0", 1387 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 1388 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 1389 | "requires": { 1390 | "ee-first": "1.1.1" 1391 | } 1392 | }, 1393 | "once": { 1394 | "version": "1.4.0", 1395 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1396 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1397 | "requires": { 1398 | "wrappy": "1" 1399 | } 1400 | }, 1401 | "os-homedir": { 1402 | "version": "1.0.2", 1403 | "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", 1404 | "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" 1405 | }, 1406 | "os-tmpdir": { 1407 | "version": "1.0.2", 1408 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 1409 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" 1410 | }, 1411 | "osenv": { 1412 | "version": "0.1.5", 1413 | "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", 1414 | "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", 1415 | "requires": { 1416 | "os-homedir": "^1.0.0", 1417 | "os-tmpdir": "^1.0.0" 1418 | } 1419 | }, 1420 | "p-finally": { 1421 | "version": "1.0.0", 1422 | "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", 1423 | "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", 1424 | "dev": true 1425 | }, 1426 | "package-json": { 1427 | "version": "4.0.1", 1428 | "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", 1429 | "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", 1430 | "dev": true, 1431 | "requires": { 1432 | "got": "^6.7.1", 1433 | "registry-auth-token": "^3.0.1", 1434 | "registry-url": "^3.0.3", 1435 | "semver": "^5.1.0" 1436 | } 1437 | }, 1438 | "parseurl": { 1439 | "version": "1.3.3", 1440 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1441 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 1442 | }, 1443 | "path-is-absolute": { 1444 | "version": "1.0.1", 1445 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1446 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 1447 | }, 1448 | "path-is-inside": { 1449 | "version": "1.0.2", 1450 | "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", 1451 | "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", 1452 | "dev": true 1453 | }, 1454 | "path-key": { 1455 | "version": "2.0.1", 1456 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 1457 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", 1458 | "dev": true 1459 | }, 1460 | "path-to-regexp": { 1461 | "version": "0.1.7", 1462 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 1463 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 1464 | }, 1465 | "performance-now": { 1466 | "version": "2.1.0", 1467 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 1468 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 1469 | }, 1470 | "picomatch": { 1471 | "version": "2.2.1", 1472 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", 1473 | "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==", 1474 | "dev": true 1475 | }, 1476 | "pify": { 1477 | "version": "3.0.0", 1478 | "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", 1479 | "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", 1480 | "dev": true 1481 | }, 1482 | "prepend-http": { 1483 | "version": "1.0.4", 1484 | "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", 1485 | "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", 1486 | "dev": true 1487 | }, 1488 | "process-nextick-args": { 1489 | "version": "2.0.1", 1490 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 1491 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 1492 | }, 1493 | "proxy-addr": { 1494 | "version": "2.0.5", 1495 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", 1496 | "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", 1497 | "requires": { 1498 | "forwarded": "~0.1.2", 1499 | "ipaddr.js": "1.9.0" 1500 | } 1501 | }, 1502 | "pseudomap": { 1503 | "version": "1.0.2", 1504 | "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", 1505 | "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", 1506 | "dev": true 1507 | }, 1508 | "psl": { 1509 | "version": "1.7.0", 1510 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", 1511 | "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" 1512 | }, 1513 | "pstree.remy": { 1514 | "version": "1.1.7", 1515 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", 1516 | "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==", 1517 | "dev": true 1518 | }, 1519 | "punycode": { 1520 | "version": "2.1.1", 1521 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1522 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 1523 | }, 1524 | "qs": { 1525 | "version": "6.9.1", 1526 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.1.tgz", 1527 | "integrity": "sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA==" 1528 | }, 1529 | "range-parser": { 1530 | "version": "1.2.1", 1531 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1532 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 1533 | }, 1534 | "raw-body": { 1535 | "version": "2.4.0", 1536 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 1537 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 1538 | "requires": { 1539 | "bytes": "3.1.0", 1540 | "http-errors": "1.7.2", 1541 | "iconv-lite": "0.4.24", 1542 | "unpipe": "1.0.0" 1543 | } 1544 | }, 1545 | "rc": { 1546 | "version": "1.2.8", 1547 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 1548 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 1549 | "requires": { 1550 | "deep-extend": "^0.6.0", 1551 | "ini": "~1.3.0", 1552 | "minimist": "^1.2.0", 1553 | "strip-json-comments": "~2.0.1" 1554 | } 1555 | }, 1556 | "readable-stream": { 1557 | "version": "3.5.0", 1558 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.5.0.tgz", 1559 | "integrity": "sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA==", 1560 | "requires": { 1561 | "inherits": "^2.0.3", 1562 | "string_decoder": "^1.1.1", 1563 | "util-deprecate": "^1.0.1" 1564 | } 1565 | }, 1566 | "readdirp": { 1567 | "version": "3.3.0", 1568 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz", 1569 | "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==", 1570 | "dev": true, 1571 | "requires": { 1572 | "picomatch": "^2.0.7" 1573 | } 1574 | }, 1575 | "registry-auth-token": { 1576 | "version": "3.4.0", 1577 | "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", 1578 | "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", 1579 | "dev": true, 1580 | "requires": { 1581 | "rc": "^1.1.6", 1582 | "safe-buffer": "^5.0.1" 1583 | } 1584 | }, 1585 | "registry-url": { 1586 | "version": "3.1.0", 1587 | "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", 1588 | "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", 1589 | "dev": true, 1590 | "requires": { 1591 | "rc": "^1.0.1" 1592 | } 1593 | }, 1594 | "request": { 1595 | "version": "2.88.0", 1596 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 1597 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 1598 | "requires": { 1599 | "aws-sign2": "~0.7.0", 1600 | "aws4": "^1.8.0", 1601 | "caseless": "~0.12.0", 1602 | "combined-stream": "~1.0.6", 1603 | "extend": "~3.0.2", 1604 | "forever-agent": "~0.6.1", 1605 | "form-data": "~2.3.2", 1606 | "har-validator": "~5.1.0", 1607 | "http-signature": "~1.2.0", 1608 | "is-typedarray": "~1.0.0", 1609 | "isstream": "~0.1.2", 1610 | "json-stringify-safe": "~5.0.1", 1611 | "mime-types": "~2.1.19", 1612 | "oauth-sign": "~0.9.0", 1613 | "performance-now": "^2.1.0", 1614 | "qs": "~6.5.2", 1615 | "safe-buffer": "^5.1.2", 1616 | "tough-cookie": "~2.4.3", 1617 | "tunnel-agent": "^0.6.0", 1618 | "uuid": "^3.3.2" 1619 | }, 1620 | "dependencies": { 1621 | "form-data": { 1622 | "version": "2.3.3", 1623 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 1624 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 1625 | "requires": { 1626 | "asynckit": "^0.4.0", 1627 | "combined-stream": "^1.0.6", 1628 | "mime-types": "^2.1.12" 1629 | } 1630 | }, 1631 | "qs": { 1632 | "version": "6.5.2", 1633 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 1634 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 1635 | } 1636 | } 1637 | }, 1638 | "rimraf": { 1639 | "version": "2.7.1", 1640 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", 1641 | "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", 1642 | "requires": { 1643 | "glob": "^7.1.3" 1644 | } 1645 | }, 1646 | "safe-buffer": { 1647 | "version": "5.2.0", 1648 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 1649 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 1650 | }, 1651 | "safer-buffer": { 1652 | "version": "2.1.2", 1653 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1654 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1655 | }, 1656 | "sax": { 1657 | "version": "1.2.4", 1658 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 1659 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" 1660 | }, 1661 | "semver": { 1662 | "version": "5.7.1", 1663 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 1664 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 1665 | }, 1666 | "semver-diff": { 1667 | "version": "2.1.0", 1668 | "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", 1669 | "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", 1670 | "dev": true, 1671 | "requires": { 1672 | "semver": "^5.0.3" 1673 | } 1674 | }, 1675 | "send": { 1676 | "version": "0.17.1", 1677 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 1678 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 1679 | "requires": { 1680 | "debug": "2.6.9", 1681 | "depd": "~1.1.2", 1682 | "destroy": "~1.0.4", 1683 | "encodeurl": "~1.0.2", 1684 | "escape-html": "~1.0.3", 1685 | "etag": "~1.8.1", 1686 | "fresh": "0.5.2", 1687 | "http-errors": "~1.7.2", 1688 | "mime": "1.6.0", 1689 | "ms": "2.1.1", 1690 | "on-finished": "~2.3.0", 1691 | "range-parser": "~1.2.1", 1692 | "statuses": "~1.5.0" 1693 | }, 1694 | "dependencies": { 1695 | "debug": { 1696 | "version": "2.6.9", 1697 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 1698 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 1699 | "requires": { 1700 | "ms": "2.0.0" 1701 | }, 1702 | "dependencies": { 1703 | "ms": { 1704 | "version": "2.0.0", 1705 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1706 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 1707 | } 1708 | } 1709 | }, 1710 | "mime": { 1711 | "version": "1.6.0", 1712 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 1713 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 1714 | }, 1715 | "ms": { 1716 | "version": "2.1.1", 1717 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 1718 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 1719 | } 1720 | } 1721 | }, 1722 | "serve-static": { 1723 | "version": "1.14.1", 1724 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 1725 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 1726 | "requires": { 1727 | "encodeurl": "~1.0.2", 1728 | "escape-html": "~1.0.3", 1729 | "parseurl": "~1.3.3", 1730 | "send": "0.17.1" 1731 | } 1732 | }, 1733 | "set-blocking": { 1734 | "version": "2.0.0", 1735 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 1736 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 1737 | }, 1738 | "setprototypeof": { 1739 | "version": "1.1.1", 1740 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 1741 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 1742 | }, 1743 | "shebang-command": { 1744 | "version": "1.2.0", 1745 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 1746 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 1747 | "dev": true, 1748 | "requires": { 1749 | "shebang-regex": "^1.0.0" 1750 | } 1751 | }, 1752 | "shebang-regex": { 1753 | "version": "1.0.0", 1754 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 1755 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", 1756 | "dev": true 1757 | }, 1758 | "signal-exit": { 1759 | "version": "3.0.2", 1760 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 1761 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 1762 | }, 1763 | "sprintf-js": { 1764 | "version": "1.0.3", 1765 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 1766 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" 1767 | }, 1768 | "sqlite3": { 1769 | "version": "4.1.1", 1770 | "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.1.tgz", 1771 | "integrity": "sha512-CvT5XY+MWnn0HkbwVKJAyWEMfzpAPwnTiB3TobA5Mri44SrTovmmh499NPQP+gatkeOipqPlBLel7rn4E/PCQg==", 1772 | "requires": { 1773 | "nan": "^2.12.1", 1774 | "node-pre-gyp": "^0.11.0", 1775 | "request": "^2.87.0" 1776 | } 1777 | }, 1778 | "sshpk": { 1779 | "version": "1.16.1", 1780 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 1781 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 1782 | "requires": { 1783 | "asn1": "~0.2.3", 1784 | "assert-plus": "^1.0.0", 1785 | "bcrypt-pbkdf": "^1.0.0", 1786 | "dashdash": "^1.12.0", 1787 | "ecc-jsbn": "~0.1.1", 1788 | "getpass": "^0.1.1", 1789 | "jsbn": "~0.1.0", 1790 | "safer-buffer": "^2.0.2", 1791 | "tweetnacl": "~0.14.0" 1792 | } 1793 | }, 1794 | "statuses": { 1795 | "version": "1.5.0", 1796 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 1797 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 1798 | }, 1799 | "string-width": { 1800 | "version": "2.1.1", 1801 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 1802 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 1803 | "requires": { 1804 | "is-fullwidth-code-point": "^2.0.0", 1805 | "strip-ansi": "^4.0.0" 1806 | } 1807 | }, 1808 | "string_decoder": { 1809 | "version": "1.3.0", 1810 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1811 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1812 | "requires": { 1813 | "safe-buffer": "~5.2.0" 1814 | } 1815 | }, 1816 | "strip-ansi": { 1817 | "version": "4.0.0", 1818 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 1819 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 1820 | "requires": { 1821 | "ansi-regex": "^3.0.0" 1822 | } 1823 | }, 1824 | "strip-eof": { 1825 | "version": "1.0.0", 1826 | "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", 1827 | "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", 1828 | "dev": true 1829 | }, 1830 | "strip-json-comments": { 1831 | "version": "2.0.1", 1832 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1833 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" 1834 | }, 1835 | "superagent": { 1836 | "version": "5.2.1", 1837 | "resolved": "https://registry.npmjs.org/superagent/-/superagent-5.2.1.tgz", 1838 | "integrity": "sha512-46b4Lkwnlz7Ebdv2FBbfuqb3kVkG1jV/SK3EW6NnwL9a3T4h5hHtegNEQfbXvTFbDoUZXId4W3dMgap2f6ic1g==", 1839 | "requires": { 1840 | "component-emitter": "^1.3.0", 1841 | "cookiejar": "^2.1.2", 1842 | "debug": "^4.1.1", 1843 | "fast-safe-stringify": "^2.0.7", 1844 | "form-data": "^3.0.0", 1845 | "formidable": "^1.2.1", 1846 | "methods": "^1.1.2", 1847 | "mime": "^2.4.4", 1848 | "qs": "^6.9.1", 1849 | "readable-stream": "^3.4.0", 1850 | "semver": "^6.3.0" 1851 | }, 1852 | "dependencies": { 1853 | "debug": { 1854 | "version": "4.1.1", 1855 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 1856 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 1857 | "requires": { 1858 | "ms": "^2.1.1" 1859 | } 1860 | }, 1861 | "semver": { 1862 | "version": "6.3.0", 1863 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 1864 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" 1865 | } 1866 | } 1867 | }, 1868 | "supports-color": { 1869 | "version": "5.5.0", 1870 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1871 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1872 | "dev": true, 1873 | "requires": { 1874 | "has-flag": "^3.0.0" 1875 | } 1876 | }, 1877 | "tail": { 1878 | "version": "2.0.3", 1879 | "resolved": "https://registry.npmjs.org/tail/-/tail-2.0.3.tgz", 1880 | "integrity": "sha512-s9NOGkLqqiDEtBttQZI7acLS8ycYK5sTlDwNjGnpXG9c8AWj0cfAtwEIzo/hVRMMiC5EYz+bXaJWC1u1u0GPpQ==" 1881 | }, 1882 | "tar": { 1883 | "version": "4.4.13", 1884 | "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", 1885 | "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", 1886 | "requires": { 1887 | "chownr": "^1.1.1", 1888 | "fs-minipass": "^1.2.5", 1889 | "minipass": "^2.8.6", 1890 | "minizlib": "^1.2.1", 1891 | "mkdirp": "^0.5.0", 1892 | "safe-buffer": "^5.1.2", 1893 | "yallist": "^3.0.3" 1894 | }, 1895 | "dependencies": { 1896 | "yallist": { 1897 | "version": "3.1.1", 1898 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 1899 | "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" 1900 | } 1901 | } 1902 | }, 1903 | "term-size": { 1904 | "version": "1.2.0", 1905 | "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", 1906 | "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", 1907 | "dev": true, 1908 | "requires": { 1909 | "execa": "^0.7.0" 1910 | } 1911 | }, 1912 | "timed-out": { 1913 | "version": "4.0.1", 1914 | "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", 1915 | "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", 1916 | "dev": true 1917 | }, 1918 | "to-regex-range": { 1919 | "version": "5.0.1", 1920 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1921 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1922 | "dev": true, 1923 | "requires": { 1924 | "is-number": "^7.0.0" 1925 | } 1926 | }, 1927 | "toidentifier": { 1928 | "version": "1.0.0", 1929 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 1930 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 1931 | }, 1932 | "touch": { 1933 | "version": "3.1.0", 1934 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", 1935 | "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", 1936 | "dev": true, 1937 | "requires": { 1938 | "nopt": "~1.0.10" 1939 | } 1940 | }, 1941 | "tough-cookie": { 1942 | "version": "2.4.3", 1943 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 1944 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 1945 | "requires": { 1946 | "psl": "^1.1.24", 1947 | "punycode": "^1.4.1" 1948 | }, 1949 | "dependencies": { 1950 | "punycode": { 1951 | "version": "1.4.1", 1952 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 1953 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 1954 | } 1955 | } 1956 | }, 1957 | "tunnel-agent": { 1958 | "version": "0.6.0", 1959 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1960 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1961 | "requires": { 1962 | "safe-buffer": "^5.0.1" 1963 | } 1964 | }, 1965 | "tweetnacl": { 1966 | "version": "0.14.5", 1967 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1968 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 1969 | }, 1970 | "type-is": { 1971 | "version": "1.6.18", 1972 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1973 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1974 | "requires": { 1975 | "media-typer": "0.3.0", 1976 | "mime-types": "~2.1.24" 1977 | } 1978 | }, 1979 | "undefsafe": { 1980 | "version": "2.0.2", 1981 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", 1982 | "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", 1983 | "dev": true, 1984 | "requires": { 1985 | "debug": "^2.2.0" 1986 | }, 1987 | "dependencies": { 1988 | "debug": { 1989 | "version": "2.6.9", 1990 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 1991 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 1992 | "dev": true, 1993 | "requires": { 1994 | "ms": "2.0.0" 1995 | } 1996 | }, 1997 | "ms": { 1998 | "version": "2.0.0", 1999 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 2000 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 2001 | "dev": true 2002 | } 2003 | } 2004 | }, 2005 | "unique-string": { 2006 | "version": "1.0.0", 2007 | "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", 2008 | "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", 2009 | "dev": true, 2010 | "requires": { 2011 | "crypto-random-string": "^1.0.0" 2012 | } 2013 | }, 2014 | "unpipe": { 2015 | "version": "1.0.0", 2016 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 2017 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 2018 | }, 2019 | "unzip-response": { 2020 | "version": "2.0.1", 2021 | "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", 2022 | "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", 2023 | "dev": true 2024 | }, 2025 | "update-notifier": { 2026 | "version": "2.5.0", 2027 | "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", 2028 | "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", 2029 | "dev": true, 2030 | "requires": { 2031 | "boxen": "^1.2.1", 2032 | "chalk": "^2.0.1", 2033 | "configstore": "^3.0.0", 2034 | "import-lazy": "^2.1.0", 2035 | "is-ci": "^1.0.10", 2036 | "is-installed-globally": "^0.1.0", 2037 | "is-npm": "^1.0.0", 2038 | "latest-version": "^3.0.0", 2039 | "semver-diff": "^2.0.0", 2040 | "xdg-basedir": "^3.0.0" 2041 | } 2042 | }, 2043 | "uri-js": { 2044 | "version": "4.2.2", 2045 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 2046 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 2047 | "requires": { 2048 | "punycode": "^2.1.0" 2049 | } 2050 | }, 2051 | "url-parse-lax": { 2052 | "version": "1.0.0", 2053 | "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", 2054 | "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", 2055 | "dev": true, 2056 | "requires": { 2057 | "prepend-http": "^1.0.1" 2058 | } 2059 | }, 2060 | "util-deprecate": { 2061 | "version": "1.0.2", 2062 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 2063 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 2064 | }, 2065 | "utils-merge": { 2066 | "version": "1.0.1", 2067 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 2068 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 2069 | }, 2070 | "uuid": { 2071 | "version": "3.4.0", 2072 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 2073 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" 2074 | }, 2075 | "vary": { 2076 | "version": "1.1.2", 2077 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 2078 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 2079 | }, 2080 | "verror": { 2081 | "version": "1.10.0", 2082 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 2083 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 2084 | "requires": { 2085 | "assert-plus": "^1.0.0", 2086 | "core-util-is": "1.0.2", 2087 | "extsprintf": "^1.2.0" 2088 | } 2089 | }, 2090 | "which": { 2091 | "version": "1.3.1", 2092 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 2093 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 2094 | "dev": true, 2095 | "requires": { 2096 | "isexe": "^2.0.0" 2097 | } 2098 | }, 2099 | "wide-align": { 2100 | "version": "1.1.3", 2101 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 2102 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 2103 | "requires": { 2104 | "string-width": "^1.0.2 || 2" 2105 | } 2106 | }, 2107 | "widest-line": { 2108 | "version": "2.0.1", 2109 | "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", 2110 | "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", 2111 | "dev": true, 2112 | "requires": { 2113 | "string-width": "^2.1.1" 2114 | } 2115 | }, 2116 | "wrappy": { 2117 | "version": "1.0.2", 2118 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 2119 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 2120 | }, 2121 | "write-file-atomic": { 2122 | "version": "2.4.3", 2123 | "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", 2124 | "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", 2125 | "dev": true, 2126 | "requires": { 2127 | "graceful-fs": "^4.1.11", 2128 | "imurmurhash": "^0.1.4", 2129 | "signal-exit": "^3.0.2" 2130 | } 2131 | }, 2132 | "xdg-basedir": { 2133 | "version": "3.0.0", 2134 | "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", 2135 | "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", 2136 | "dev": true 2137 | }, 2138 | "xml2js": { 2139 | "version": "0.4.23", 2140 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", 2141 | "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", 2142 | "requires": { 2143 | "sax": ">=0.6.0", 2144 | "xmlbuilder": "~11.0.0" 2145 | } 2146 | }, 2147 | "xmlbuilder": { 2148 | "version": "11.0.1", 2149 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", 2150 | "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" 2151 | }, 2152 | "yallist": { 2153 | "version": "2.1.2", 2154 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", 2155 | "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", 2156 | "dev": true 2157 | } 2158 | } 2159 | } 2160 | --------------------------------------------------------------------------------