├── .dockerignore ├── .env ├── .gitignore ├── .npmrc ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── bin └── server ├── lib ├── Client.js ├── Client.test.js ├── ClientManager.js ├── ClientManager.test.js ├── TunnelAgent.js └── TunnelAgent.test.js ├── package.json ├── server.js ├── server.test.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DEBUG=localtunnel* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "9.2" 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.1.0-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json /app/ 6 | COPY yarn.lock /app/ 7 | 8 | RUN yarn install --production && yarn cache clean 9 | 10 | COPY . /app 11 | 12 | ENV NODE_ENV production 13 | ENTRYPOINT ["node", "-r", "esm", "./bin/server"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Roman Shtylman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # localtunnel-server 2 | 3 | [![Build Status](https://travis-ci.org/localtunnel/server.svg?branch=master)](https://travis-ci.org/localtunnel/server) 4 | 5 | localtunnel exposes your localhost to the world for easy testing and sharing! No need to mess with DNS or deploy just to have others test out your changes. 6 | 7 | This repo is the server component. If you are just looking for the CLI localtunnel app, see (https://github.com/localtunnel/localtunnel). 8 | 9 | ## overview ## 10 | 11 | The default localtunnel client connects to the `localtunnel.me` server. You can, however, easily set up and run your own server. In order to run your own localtunnel server you must ensure that your server can meet the following requirements: 12 | 13 | * You can set up DNS entries for your `domain.tld` and `*.domain.tld` (or `sub.domain.tld` and `*.sub.domain.tld`). 14 | * The server can accept incoming TCP connections for any non-root TCP port (i.e. ports over 1000). 15 | 16 | The above are important as the client will ask the server for a subdomain under a particular domain. The server will listen on any OS-assigned TCP port for client connections. 17 | 18 | #### setup 19 | 20 | ```shell 21 | # pick a place where the files will live 22 | git clone git://github.com/defunctzombie/localtunnel-server.git 23 | cd localtunnel-server 24 | npm install 25 | 26 | # server set to run on port 1234 27 | bin/server --port 1234 28 | ``` 29 | 30 | The localtunnel server is now running and waiting for client requests on port 1234. You will most likely want to set up a reverse proxy to listen on port 80 (or start localtunnel on port 80 directly). 31 | 32 | **NOTE** By default, localtunnel will use subdomains for clients, if you plan to host your localtunnel server itself on a subdomain you will need to use the _--domain_ option and specify the domain name behind which you are hosting localtunnel. (i.e. my-localtunnel-server.example.com) 33 | 34 | #### use your server 35 | 36 | You can now use your domain with the `--host` flag for the `lt` client. 37 | 38 | ```shell 39 | lt --host http://sub.example.tld:1234 --port 9000 40 | ``` 41 | 42 | You will be assigned a URL similar to `heavy-puma-9.sub.example.com:1234`. 43 | 44 | If your server is acting as a reverse proxy (i.e. nginx) and is able to listen on port 80, then you do not need the `:1234` part of the hostname for the `lt` client. 45 | 46 | ## REST API 47 | 48 | ### POST /api/tunnels 49 | 50 | Create a new tunnel. A LocalTunnel client posts to this enpoint to request a new tunnel with a specific name or a randomly assigned name. 51 | 52 | ### GET /api/status 53 | 54 | General server information. 55 | 56 | ## Deploy 57 | 58 | You can deploy your own localtunnel server using the prebuilt docker image. 59 | 60 | **Note** This assumes that you have a proxy in front of the server to handle the http(s) requests and forward them to the localtunnel server on port 3000. You can use our [localtunnel-nginx](https://github.com/localtunnel/nginx) to accomplish this. 61 | 62 | If you do not want ssl support for your own tunnel (not recommended), then you can just run the below with `--port 80` instead. 63 | 64 | ``` 65 | docker run -d \ 66 | --restart always \ 67 | --name localtunnel \ 68 | --net host \ 69 | defunctzombie/localtunnel-server:latest --port 3000 70 | ``` 71 | -------------------------------------------------------------------------------- /bin/server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node -r esm 2 | 3 | import 'localenv'; 4 | import optimist from 'optimist'; 5 | 6 | import log from 'book'; 7 | import Debug from 'debug'; 8 | 9 | import CreateServer from '../server'; 10 | 11 | const debug = Debug('localtunnel'); 12 | 13 | const argv = optimist 14 | .usage('Usage: $0 --port [num]') 15 | .options('secure', { 16 | default: false, 17 | describe: 'use this flag to indicate proxy over https' 18 | }) 19 | .options('port', { 20 | default: '80', 21 | describe: 'listen on this port for outside requests' 22 | }) 23 | .options('address', { 24 | default: '0.0.0.0', 25 | describe: 'IP address to bind to' 26 | }) 27 | .options('domain', { 28 | describe: 'Specify the base domain name. This is optional if hosting localtunnel from a regular example.com domain. This is required if hosting a localtunnel server from a subdomain (i.e. lt.example.dom where clients will be client-app.lt.example.come)', 29 | }) 30 | .options('max-sockets', { 31 | default: 10, 32 | describe: 'maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)' 33 | }) 34 | .argv; 35 | 36 | if (argv.help) { 37 | optimist.showHelp(); 38 | process.exit(); 39 | } 40 | 41 | const server = CreateServer({ 42 | max_tcp_sockets: argv['max-sockets'], 43 | secure: argv.secure, 44 | domain: argv.domain, 45 | }); 46 | 47 | server.listen(argv.port, argv.address, () => { 48 | debug('server listening on port: %d', server.address().port); 49 | }); 50 | 51 | process.on('SIGINT', () => { 52 | process.exit(); 53 | }); 54 | 55 | process.on('SIGTERM', () => { 56 | process.exit(); 57 | }); 58 | 59 | process.on('uncaughtException', (err) => { 60 | log.error(err); 61 | }); 62 | 63 | process.on('unhandledRejection', (reason, promise) => { 64 | log.error(reason); 65 | }); 66 | 67 | // vim: ft=javascript 68 | 69 | -------------------------------------------------------------------------------- /lib/Client.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import Debug from 'debug'; 3 | import pump from 'pump'; 4 | import EventEmitter from 'events'; 5 | 6 | // A client encapsulates req/res handling using an agent 7 | // 8 | // If an agent is destroyed, the request handling will error 9 | // The caller is responsible for handling a failed request 10 | class Client extends EventEmitter { 11 | constructor(options) { 12 | super(); 13 | 14 | const agent = this.agent = options.agent; 15 | const id = this.id = options.id; 16 | 17 | this.debug = Debug(`lt:Client[${this.id}]`); 18 | 19 | // client is given a grace period in which they can connect before they are _removed_ 20 | this.graceTimeout = setTimeout(() => { 21 | this.close(); 22 | }, 1000).unref(); 23 | 24 | agent.on('online', () => { 25 | this.debug('client online %s', id); 26 | clearTimeout(this.graceTimeout); 27 | }); 28 | 29 | agent.on('offline', () => { 30 | this.debug('client offline %s', id); 31 | 32 | // if there was a previous timeout set, we don't want to double trigger 33 | clearTimeout(this.graceTimeout); 34 | 35 | // client is given a grace period in which they can re-connect before they are _removed_ 36 | this.graceTimeout = setTimeout(() => { 37 | this.close(); 38 | }, 1000).unref(); 39 | }); 40 | 41 | // TODO(roman): an agent error removes the client, the user needs to re-connect? 42 | // how does a user realize they need to re-connect vs some random client being assigned same port? 43 | agent.once('error', (err) => { 44 | this.close(); 45 | }); 46 | } 47 | 48 | stats() { 49 | return this.agent.stats(); 50 | } 51 | 52 | close() { 53 | clearTimeout(this.graceTimeout); 54 | this.agent.destroy(); 55 | this.emit('close'); 56 | } 57 | 58 | handleRequest(req, res) { 59 | this.debug('> %s', req.url); 60 | const opt = { 61 | path: req.url, 62 | agent: this.agent, 63 | method: req.method, 64 | headers: req.headers 65 | }; 66 | 67 | const clientReq = http.request(opt, (clientRes) => { 68 | this.debug('< %s', req.url); 69 | // write response code and headers 70 | res.writeHead(clientRes.statusCode, clientRes.headers); 71 | 72 | // using pump is deliberate - see the pump docs for why 73 | pump(clientRes, res); 74 | }); 75 | 76 | // this can happen when underlying agent produces an error 77 | // in our case we 504 gateway error this? 78 | // if we have already sent headers? 79 | clientReq.once('error', (err) => { 80 | // TODO(roman): if headers not sent - respond with gateway unavailable 81 | }); 82 | 83 | // using pump is deliberate - see the pump docs for why 84 | pump(req, clientReq); 85 | } 86 | 87 | handleUpgrade(req, socket) { 88 | this.debug('> [up] %s', req.url); 89 | socket.once('error', (err) => { 90 | // These client side errors can happen if the client dies while we are reading 91 | // We don't need to surface these in our logs. 92 | if (err.code == 'ECONNRESET' || err.code == 'ETIMEDOUT') { 93 | return; 94 | } 95 | console.error(err); 96 | }); 97 | 98 | this.agent.createConnection({}, (err, conn) => { 99 | this.debug('< [up] %s', req.url); 100 | // any errors getting a connection mean we cannot service this request 101 | if (err) { 102 | socket.end(); 103 | return; 104 | } 105 | 106 | // socket met have disconnected while we waiting for a socket 107 | if (!socket.readable || !socket.writable) { 108 | conn.destroy(); 109 | socket.end(); 110 | return; 111 | } 112 | 113 | // websocket requests are special in that we simply re-create the header info 114 | // then directly pipe the socket data 115 | // avoids having to rebuild the request and handle upgrades via the http client 116 | const arr = [`${req.method} ${req.url} HTTP/${req.httpVersion}`]; 117 | for (let i=0 ; i < (req.rawHeaders.length-1) ; i+=2) { 118 | arr.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}`); 119 | } 120 | 121 | arr.push(''); 122 | arr.push(''); 123 | 124 | // using pump is deliberate - see the pump docs for why 125 | pump(conn, socket); 126 | pump(socket, conn); 127 | conn.write(arr.join('\r\n')); 128 | }); 129 | } 130 | } 131 | 132 | export default Client; -------------------------------------------------------------------------------- /lib/Client.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import http from 'http'; 3 | import { Duplex } from 'stream'; 4 | import WebSocket from 'ws'; 5 | import net from 'net'; 6 | 7 | import Client from './Client'; 8 | 9 | class DummySocket extends Duplex { 10 | constructor(options) { 11 | super(options); 12 | } 13 | 14 | _write(chunk, encoding, callback) { 15 | callback(); 16 | } 17 | 18 | _read(size) { 19 | this.push('HTTP/1.1 304 Not Modified\r\nX-Powered-By: dummy\r\n\r\n\r\n'); 20 | this.push(null); 21 | } 22 | } 23 | 24 | class DummyWebsocket extends Duplex { 25 | constructor(options) { 26 | super(options); 27 | this.sentHeader = false; 28 | } 29 | 30 | _write(chunk, encoding, callback) { 31 | const str = chunk.toString(); 32 | // if chunk contains `GET / HTTP/1.1` -> queue headers 33 | // otherwise echo back received data 34 | if (str.indexOf('GET / HTTP/1.1') === 0) { 35 | const arr = [ 36 | 'HTTP/1.1 101 Switching Protocols', 37 | 'Upgrade: websocket', 38 | 'Connection: Upgrade', 39 | ]; 40 | this.push(arr.join('\r\n')); 41 | this.push('\r\n\r\n'); 42 | } 43 | else { 44 | this.push(str); 45 | } 46 | callback(); 47 | } 48 | 49 | _read(size) { 50 | // nothing to implement 51 | } 52 | } 53 | 54 | class DummyAgent extends http.Agent { 55 | constructor() { 56 | super(); 57 | } 58 | 59 | createConnection(options, cb) { 60 | cb(null, new DummySocket()); 61 | } 62 | } 63 | 64 | describe('Client', () => { 65 | it('should handle request', async () => { 66 | const agent = new DummyAgent(); 67 | const client = new Client({ agent }); 68 | 69 | const server = http.createServer((req, res) => { 70 | client.handleRequest(req, res); 71 | }); 72 | 73 | await new Promise(resolve => server.listen(resolve)); 74 | 75 | const address = server.address(); 76 | const opt = { 77 | host: 'localhost', 78 | port: address.port, 79 | path: '/', 80 | }; 81 | 82 | const res = await new Promise((resolve) => { 83 | const req = http.get(opt, (res) => { 84 | resolve(res); 85 | }); 86 | req.end(); 87 | }); 88 | assert.equal(res.headers['x-powered-by'], 'dummy'); 89 | server.close(); 90 | }); 91 | 92 | it('should handle upgrade', async () => { 93 | // need a websocket server and a socket for it 94 | class DummyWebsocketAgent extends http.Agent { 95 | constructor() { 96 | super(); 97 | } 98 | 99 | createConnection(options, cb) { 100 | cb(null, new DummyWebsocket()); 101 | } 102 | } 103 | 104 | const agent = new DummyWebsocketAgent(); 105 | const client = new Client({ agent }); 106 | 107 | const server = http.createServer(); 108 | server.on('upgrade', (req, socket, head) => { 109 | client.handleUpgrade(req, socket); 110 | }); 111 | 112 | await new Promise(resolve => server.listen(resolve)); 113 | 114 | const address = server.address(); 115 | 116 | const netClient = await new Promise((resolve) => { 117 | const newClient = net.createConnection({ port: address.port }, () => { 118 | resolve(newClient); 119 | }); 120 | }); 121 | 122 | const out = [ 123 | 'GET / HTTP/1.1', 124 | 'Connection: Upgrade', 125 | 'Upgrade: websocket' 126 | ]; 127 | 128 | netClient.write(out.join('\r\n') + '\r\n\r\n'); 129 | 130 | { 131 | const data = await new Promise((resolve) => { 132 | netClient.once('data', (chunk) => { 133 | resolve(chunk.toString()); 134 | }); 135 | }); 136 | const exp = [ 137 | 'HTTP/1.1 101 Switching Protocols', 138 | 'Upgrade: websocket', 139 | 'Connection: Upgrade', 140 | ]; 141 | assert.equal(exp.join('\r\n') + '\r\n\r\n', data); 142 | } 143 | 144 | { 145 | netClient.write('foobar'); 146 | const data = await new Promise((resolve) => { 147 | netClient.once('data', (chunk) => { 148 | resolve(chunk.toString()); 149 | }); 150 | }); 151 | assert.equal('foobar', data); 152 | } 153 | 154 | netClient.destroy(); 155 | server.close(); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /lib/ClientManager.js: -------------------------------------------------------------------------------- 1 | import { hri } from 'human-readable-ids'; 2 | import Debug from 'debug'; 3 | 4 | import Client from './Client'; 5 | import TunnelAgent from './TunnelAgent'; 6 | 7 | // Manage sets of clients 8 | // 9 | // A client is a "user session" established to service a remote localtunnel client 10 | class ClientManager { 11 | constructor(opt) { 12 | this.opt = opt || {}; 13 | 14 | // id -> client instance 15 | this.clients = new Map(); 16 | 17 | // statistics 18 | this.stats = { 19 | tunnels: 0 20 | }; 21 | 22 | this.debug = Debug('lt:ClientManager'); 23 | 24 | // This is totally wrong :facepalm: this needs to be per-client... 25 | this.graceTimeout = null; 26 | } 27 | 28 | // create a new tunnel with `id` 29 | // if the id is already used, a random id is assigned 30 | // if the tunnel could not be created, throws an error 31 | async newClient(id) { 32 | const clients = this.clients; 33 | const stats = this.stats; 34 | 35 | // can't ask for id already is use 36 | if (clients[id]) { 37 | id = hri.random(); 38 | } 39 | 40 | const maxSockets = this.opt.max_tcp_sockets; 41 | const agent = new TunnelAgent({ 42 | clientId: id, 43 | maxSockets: 10, 44 | }); 45 | 46 | const client = new Client({ 47 | id, 48 | agent, 49 | }); 50 | 51 | // add to clients map immediately 52 | // avoiding races with other clients requesting same id 53 | clients[id] = client; 54 | 55 | client.once('close', () => { 56 | this.removeClient(id); 57 | }); 58 | 59 | // try/catch used here to remove client id 60 | try { 61 | const info = await agent.listen(); 62 | ++stats.tunnels; 63 | return { 64 | id: id, 65 | port: info.port, 66 | max_conn_count: maxSockets, 67 | }; 68 | } 69 | catch (err) { 70 | this.removeClient(id); 71 | // rethrow error for upstream to handle 72 | throw err; 73 | } 74 | } 75 | 76 | removeClient(id) { 77 | this.debug('removing client: %s', id); 78 | const client = this.clients[id]; 79 | if (!client) { 80 | return; 81 | } 82 | --this.stats.tunnels; 83 | delete this.clients[id]; 84 | client.close(); 85 | } 86 | 87 | hasClient(id) { 88 | return !!this.clients[id]; 89 | } 90 | 91 | getClient(id) { 92 | return this.clients[id]; 93 | } 94 | } 95 | 96 | export default ClientManager; 97 | -------------------------------------------------------------------------------- /lib/ClientManager.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import net from 'net'; 3 | 4 | import ClientManager from './ClientManager'; 5 | 6 | describe('ClientManager', () => { 7 | it('should construct with no tunnels', () => { 8 | const manager = new ClientManager(); 9 | assert.equal(manager.stats.tunnels, 0); 10 | }); 11 | 12 | it('should create a new client with random id', async () => { 13 | const manager = new ClientManager(); 14 | const client = await manager.newClient(); 15 | assert(manager.hasClient(client.id)); 16 | manager.removeClient(client.id); 17 | }); 18 | 19 | it('should create a new client with id', async () => { 20 | const manager = new ClientManager(); 21 | const client = await manager.newClient('foobar'); 22 | assert(manager.hasClient('foobar')); 23 | manager.removeClient('foobar'); 24 | }); 25 | 26 | it('should create a new client with random id if previous exists', async () => { 27 | const manager = new ClientManager(); 28 | const clientA = await manager.newClient('foobar'); 29 | const clientB = await manager.newClient('foobar'); 30 | assert(clientA.id, 'foobar'); 31 | assert(manager.hasClient(clientB.id)); 32 | assert(clientB.id != clientA.id); 33 | manager.removeClient(clientB.id); 34 | manager.removeClient('foobar'); 35 | }); 36 | 37 | it('should remove client once it goes offline', async () => { 38 | const manager = new ClientManager(); 39 | const client = await manager.newClient('foobar'); 40 | 41 | const socket = await new Promise((resolve) => { 42 | const netClient = net.createConnection({ port: client.port }, () => { 43 | resolve(netClient); 44 | }); 45 | }); 46 | const closePromise = new Promise(resolve => socket.once('close', resolve)); 47 | socket.end(); 48 | await closePromise; 49 | 50 | // should still have client - grace period has not expired 51 | assert(manager.hasClient('foobar')); 52 | 53 | // wait past grace period (1s) 54 | await new Promise(resolve => setTimeout(resolve, 1500)); 55 | assert(!manager.hasClient('foobar')); 56 | }).timeout(5000); 57 | 58 | it('should remove correct client once it goes offline', async () => { 59 | const manager = new ClientManager(); 60 | const clientFoo = await manager.newClient('foo'); 61 | const clientBar = await manager.newClient('bar'); 62 | 63 | const socket = await new Promise((resolve) => { 64 | const netClient = net.createConnection({ port: clientFoo.port }, () => { 65 | resolve(netClient); 66 | }); 67 | }); 68 | 69 | await new Promise(resolve => setTimeout(resolve, 1500)); 70 | 71 | // foo should still be ok 72 | assert(manager.hasClient('foo')); 73 | 74 | // clientBar shound be removed - nothing connected to it 75 | assert(!manager.hasClient('bar')); 76 | 77 | manager.removeClient('foo'); 78 | socket.end(); 79 | }).timeout(5000); 80 | 81 | it('should remove clients if they do not connect within 5 seconds', async () => { 82 | const manager = new ClientManager(); 83 | const clientFoo = await manager.newClient('foo'); 84 | assert(manager.hasClient('foo')); 85 | 86 | // wait past grace period (1s) 87 | await new Promise(resolve => setTimeout(resolve, 1500)); 88 | assert(!manager.hasClient('foo')); 89 | }).timeout(5000); 90 | }); 91 | -------------------------------------------------------------------------------- /lib/TunnelAgent.js: -------------------------------------------------------------------------------- 1 | import { Agent } from 'http'; 2 | import net from 'net'; 3 | import assert from 'assert'; 4 | import log from 'book'; 5 | import Debug from 'debug'; 6 | 7 | const DEFAULT_MAX_SOCKETS = 10; 8 | 9 | // Implements an http.Agent interface to a pool of tunnel sockets 10 | // A tunnel socket is a connection _from_ a client that will 11 | // service http requests. This agent is usable wherever one can use an http.Agent 12 | class TunnelAgent extends Agent { 13 | constructor(options = {}) { 14 | super({ 15 | keepAlive: true, 16 | // only allow keepalive to hold on to one socket 17 | // this prevents it from holding on to all the sockets so they can be used for upgrades 18 | maxFreeSockets: 1, 19 | }); 20 | 21 | // sockets we can hand out via createConnection 22 | this.availableSockets = []; 23 | 24 | // when a createConnection cannot return a socket, it goes into a queue 25 | // once a socket is available it is handed out to the next callback 26 | this.waitingCreateConn = []; 27 | 28 | this.debug = Debug(`lt:TunnelAgent[${options.clientId}]`); 29 | 30 | // track maximum allowed sockets 31 | this.connectedSockets = 0; 32 | this.maxTcpSockets = options.maxTcpSockets || DEFAULT_MAX_SOCKETS; 33 | 34 | // new tcp server to service requests for this client 35 | this.server = net.createServer(); 36 | 37 | // flag to avoid double starts 38 | this.started = false; 39 | this.closed = false; 40 | } 41 | 42 | stats() { 43 | return { 44 | connectedSockets: this.connectedSockets, 45 | }; 46 | } 47 | 48 | listen() { 49 | const server = this.server; 50 | if (this.started) { 51 | throw new Error('already started'); 52 | } 53 | this.started = true; 54 | 55 | server.on('close', this._onClose.bind(this)); 56 | server.on('connection', this._onConnection.bind(this)); 57 | server.on('error', (err) => { 58 | // These errors happen from killed connections, we don't worry about them 59 | if (err.code == 'ECONNRESET' || err.code == 'ETIMEDOUT') { 60 | return; 61 | } 62 | log.error(err); 63 | }); 64 | 65 | return new Promise((resolve) => { 66 | server.listen(() => { 67 | const port = server.address().port; 68 | this.debug('tcp server listening on port: %d', port); 69 | 70 | resolve({ 71 | // port for lt client tcp connections 72 | port: port, 73 | }); 74 | }); 75 | }); 76 | } 77 | 78 | _onClose() { 79 | this.closed = true; 80 | this.debug('closed tcp socket'); 81 | // flush any waiting connections 82 | for (const conn of this.waitingCreateConn) { 83 | conn(new Error('closed'), null); 84 | } 85 | this.waitingCreateConn = []; 86 | this.emit('end'); 87 | } 88 | 89 | // new socket connection from client for tunneling requests to client 90 | _onConnection(socket) { 91 | // no more socket connections allowed 92 | if (this.connectedSockets >= this.maxTcpSockets) { 93 | this.debug('no more sockets allowed'); 94 | socket.destroy(); 95 | return false; 96 | } 97 | 98 | socket.once('close', (hadError) => { 99 | this.debug('closed socket (error: %s)', hadError); 100 | this.connectedSockets -= 1; 101 | // remove the socket from available list 102 | const idx = this.availableSockets.indexOf(socket); 103 | if (idx >= 0) { 104 | this.availableSockets.splice(idx, 1); 105 | } 106 | 107 | this.debug('connected sockets: %s', this.connectedSockets); 108 | if (this.connectedSockets <= 0) { 109 | this.debug('all sockets disconnected'); 110 | this.emit('offline'); 111 | } 112 | }); 113 | 114 | // close will be emitted after this 115 | socket.once('error', (err) => { 116 | // we do not log these errors, sessions can drop from clients for many reasons 117 | // these are not actionable errors for our server 118 | socket.destroy(); 119 | }); 120 | 121 | if (this.connectedSockets === 0) { 122 | this.emit('online'); 123 | } 124 | 125 | this.connectedSockets += 1; 126 | this.debug('new connection from: %s:%s', socket.address().address, socket.address().port); 127 | 128 | // if there are queued callbacks, give this socket now and don't queue into available 129 | const fn = this.waitingCreateConn.shift(); 130 | if (fn) { 131 | this.debug('giving socket to queued conn request'); 132 | setTimeout(() => { 133 | fn(null, socket); 134 | }, 0); 135 | return; 136 | } 137 | 138 | // make socket available for those waiting on sockets 139 | this.availableSockets.push(socket); 140 | } 141 | 142 | // fetch a socket from the available socket pool for the agent 143 | // if no socket is available, queue 144 | // cb(err, socket) 145 | createConnection(options, cb) { 146 | if (this.closed) { 147 | cb(new Error('closed')); 148 | return; 149 | } 150 | 151 | this.debug('create connection'); 152 | 153 | // socket is a tcp connection back to the user hosting the site 154 | const sock = this.availableSockets.shift(); 155 | 156 | // no available sockets 157 | // wait until we have one 158 | if (!sock) { 159 | this.waitingCreateConn.push(cb); 160 | this.debug('waiting connected: %s', this.connectedSockets); 161 | this.debug('waiting available: %s', this.availableSockets.length); 162 | return; 163 | } 164 | 165 | this.debug('socket given'); 166 | cb(null, sock); 167 | } 168 | 169 | destroy() { 170 | this.server.close(); 171 | super.destroy(); 172 | } 173 | } 174 | 175 | export default TunnelAgent; 176 | -------------------------------------------------------------------------------- /lib/TunnelAgent.test.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import net from 'net'; 3 | import assert from 'assert'; 4 | 5 | import TunnelAgent from './TunnelAgent'; 6 | 7 | describe('TunnelAgent', () => { 8 | it('should create an empty agent', async () => { 9 | const agent = new TunnelAgent(); 10 | assert.equal(agent.started, false); 11 | 12 | const info = await agent.listen(); 13 | assert.ok(info.port > 0); 14 | agent.destroy(); 15 | }); 16 | 17 | it('should create a new server and accept connections', async () => { 18 | const agent = new TunnelAgent(); 19 | assert.equal(agent.started, false); 20 | 21 | const info = await agent.listen(); 22 | const sock = net.createConnection({ port: info.port }); 23 | 24 | // in this test we wait for the socket to be connected 25 | await new Promise(resolve => sock.once('connect', resolve)); 26 | 27 | const agentSock = await new Promise((resolve, reject) => { 28 | agent.createConnection({}, (err, sock) => { 29 | if (err) { 30 | reject(err); 31 | } 32 | resolve(sock); 33 | }); 34 | }); 35 | 36 | agentSock.write('foo'); 37 | await new Promise(resolve => sock.once('readable', resolve)); 38 | 39 | assert.equal('foo', sock.read().toString()); 40 | agent.destroy(); 41 | sock.destroy(); 42 | }); 43 | 44 | it('should reject connections over the max', async () => { 45 | const agent = new TunnelAgent({ 46 | maxTcpSockets: 2, 47 | }); 48 | assert.equal(agent.started, false); 49 | 50 | const info = await agent.listen(); 51 | const sock1 = net.createConnection({ port: info.port }); 52 | const sock2 = net.createConnection({ port: info.port }); 53 | 54 | // two valid socket connections 55 | const p1 = new Promise(resolve => sock1.once('connect', resolve)); 56 | const p2 = new Promise(resolve => sock2.once('connect', resolve)); 57 | await Promise.all([p1, p2]); 58 | 59 | const sock3 = net.createConnection({ port: info.port }); 60 | const p3 = await new Promise(resolve => sock3.once('close', resolve)); 61 | 62 | agent.destroy(); 63 | sock1.destroy(); 64 | sock2.destroy(); 65 | sock3.destroy(); 66 | }); 67 | 68 | it('should queue createConnection requests', async () => { 69 | const agent = new TunnelAgent(); 70 | assert.equal(agent.started, false); 71 | 72 | const info = await agent.listen(); 73 | 74 | // create a promise for the next connection 75 | let fulfilled = false; 76 | const waitSockPromise = new Promise((resolve, reject) => { 77 | agent.createConnection({}, (err, sock) => { 78 | fulfilled = true; 79 | if (err) { 80 | reject(err); 81 | } 82 | resolve(sock); 83 | }); 84 | }); 85 | 86 | // check that the next socket is not yet available 87 | await new Promise(resolve => setTimeout(resolve, 500)); 88 | assert(!fulfilled); 89 | 90 | // connect, this will make a socket available 91 | const sock = net.createConnection({ port: info.port }); 92 | await new Promise(resolve => sock.once('connect', resolve)); 93 | 94 | const anotherAgentSock = await waitSockPromise; 95 | agent.destroy(); 96 | sock.destroy(); 97 | }); 98 | 99 | it('should should emit a online event when a socket connects', async () => { 100 | const agent = new TunnelAgent(); 101 | const info = await agent.listen(); 102 | 103 | const onlinePromise = new Promise(resolve => agent.once('online', resolve)); 104 | 105 | const sock = net.createConnection({ port: info.port }); 106 | await new Promise(resolve => sock.once('connect', resolve)); 107 | 108 | await onlinePromise; 109 | agent.destroy(); 110 | sock.destroy(); 111 | }); 112 | 113 | it('should emit offline event when socket disconnects', async () => { 114 | const agent = new TunnelAgent(); 115 | const info = await agent.listen(); 116 | 117 | const offlinePromise = new Promise(resolve => agent.once('offline', resolve)); 118 | 119 | const sock = net.createConnection({ port: info.port }); 120 | await new Promise(resolve => sock.once('connect', resolve)); 121 | 122 | sock.end(); 123 | await offlinePromise; 124 | agent.destroy(); 125 | sock.destroy(); 126 | }); 127 | 128 | it('should emit offline event only when last socket disconnects', async () => { 129 | const agent = new TunnelAgent(); 130 | const info = await agent.listen(); 131 | 132 | const offlinePromise = new Promise(resolve => agent.once('offline', resolve)); 133 | 134 | const sockA = net.createConnection({ port: info.port }); 135 | await new Promise(resolve => sockA.once('connect', resolve)); 136 | const sockB = net.createConnection({ port: info.port }); 137 | await new Promise(resolve => sockB.once('connect', resolve)); 138 | 139 | sockA.end(); 140 | 141 | const timeout = new Promise(resolve => setTimeout(resolve, 500)); 142 | await Promise.race([offlinePromise, timeout]); 143 | 144 | sockB.end(); 145 | await offlinePromise; 146 | 147 | agent.destroy(); 148 | }); 149 | 150 | it('should error an http request', async () => { 151 | class ErrorAgent extends http.Agent { 152 | constructor() { 153 | super(); 154 | } 155 | 156 | createConnection(options, cb) { 157 | cb(new Error('foo')); 158 | } 159 | } 160 | 161 | const agent = new ErrorAgent(); 162 | 163 | const opt = { 164 | host: 'localhost', 165 | port: 1234, 166 | path: '/', 167 | agent: agent, 168 | }; 169 | 170 | const err = await new Promise((resolve) => { 171 | const req = http.get(opt, (res) => {}); 172 | req.once('error', resolve); 173 | }); 174 | assert.equal(err.message, 'foo'); 175 | }); 176 | 177 | it('should return stats', async () => { 178 | const agent = new TunnelAgent(); 179 | assert.deepEqual(agent.stats(), { 180 | connectedSockets: 0, 181 | }); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Roman Shtylman ", 3 | "name": "localtunnel-server", 4 | "description": "expose localhost to the world", 5 | "version": "0.0.8", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/localtunnel/server.git" 10 | }, 11 | "dependencies": { 12 | "book": "1.3.3", 13 | "debug": "3.1.0", 14 | "esm": "3.0.34", 15 | "human-readable-ids": "1.0.3", 16 | "koa": "2.5.1", 17 | "koa-router": "7.4.0", 18 | "localenv": "0.2.2", 19 | "optimist": "0.6.1", 20 | "pump": "3.0.0", 21 | "tldjs": "2.3.1" 22 | }, 23 | "devDependencies": { 24 | "mocha": "5.1.1", 25 | "node-dev": "3.1.3", 26 | "supertest": "3.1.0", 27 | "ws": "5.1.1" 28 | }, 29 | "scripts": { 30 | "test": "mocha --check-leaks --require esm './**/*.test.js'", 31 | "start": "./bin/server", 32 | "dev": "node-dev bin/server --port 3000" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import log from 'book'; 2 | import Koa from 'koa'; 3 | import tldjs from 'tldjs'; 4 | import Debug from 'debug'; 5 | import http from 'http'; 6 | import { hri } from 'human-readable-ids'; 7 | import Router from 'koa-router'; 8 | 9 | import ClientManager from './lib/ClientManager'; 10 | 11 | const debug = Debug('localtunnel:server'); 12 | 13 | export default function(opt) { 14 | opt = opt || {}; 15 | 16 | const validHosts = (opt.domain) ? [opt.domain] : undefined; 17 | const myTldjs = tldjs.fromUserSettings({ validHosts }); 18 | const landingPage = opt.landing || 'https://localtunnel.github.io/www/'; 19 | 20 | function GetClientIdFromHostname(hostname) { 21 | return myTldjs.getSubdomain(hostname); 22 | } 23 | 24 | const manager = new ClientManager(opt); 25 | 26 | const schema = opt.secure ? 'https' : 'http'; 27 | 28 | const app = new Koa(); 29 | const router = new Router(); 30 | 31 | router.get('/api/status', async (ctx, next) => { 32 | const stats = manager.stats; 33 | ctx.body = { 34 | tunnels: stats.tunnels, 35 | mem: process.memoryUsage(), 36 | }; 37 | }); 38 | 39 | router.get('/api/tunnels/:id/status', async (ctx, next) => { 40 | const clientId = ctx.params.id; 41 | const client = manager.getClient(clientId); 42 | if (!client) { 43 | ctx.throw(404); 44 | return; 45 | } 46 | 47 | const stats = client.stats(); 48 | ctx.body = { 49 | connected_sockets: stats.connectedSockets, 50 | }; 51 | }); 52 | 53 | app.use(router.routes()); 54 | app.use(router.allowedMethods()); 55 | 56 | // root endpoint 57 | app.use(async (ctx, next) => { 58 | const path = ctx.request.path; 59 | 60 | // skip anything not on the root path 61 | if (path !== '/') { 62 | await next(); 63 | return; 64 | } 65 | 66 | const isNewClientRequest = ctx.query['new'] !== undefined; 67 | if (isNewClientRequest) { 68 | const reqId = hri.random(); 69 | debug('making new client with id %s', reqId); 70 | const info = await manager.newClient(reqId); 71 | 72 | const url = schema + '://' + info.id + '.' + ctx.request.host; 73 | info.url = url; 74 | ctx.body = info; 75 | return; 76 | } 77 | 78 | // no new client request, send to landing page 79 | ctx.redirect(landingPage); 80 | }); 81 | 82 | // anything after the / path is a request for a specific client name 83 | // This is a backwards compat feature 84 | app.use(async (ctx, next) => { 85 | const parts = ctx.request.path.split('/'); 86 | 87 | // any request with several layers of paths is not allowed 88 | // rejects /foo/bar 89 | // allow /foo 90 | if (parts.length !== 2) { 91 | await next(); 92 | return; 93 | } 94 | 95 | const reqId = parts[1]; 96 | 97 | // limit requested hostnames to 63 characters 98 | if (! /^(?:[a-z0-9][a-z0-9\-]{4,63}[a-z0-9]|[a-z0-9]{4,63})$/.test(reqId)) { 99 | const msg = 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'; 100 | ctx.status = 403; 101 | ctx.body = { 102 | message: msg, 103 | }; 104 | return; 105 | } 106 | 107 | debug('making new client with id %s', reqId); 108 | const info = await manager.newClient(reqId); 109 | 110 | const url = schema + '://' + info.id + '.' + ctx.request.host; 111 | info.url = url; 112 | ctx.body = info; 113 | return; 114 | }); 115 | 116 | const server = http.createServer(); 117 | 118 | const appCallback = app.callback(); 119 | 120 | server.on('request', (req, res) => { 121 | // without a hostname, we won't know who the request is for 122 | const hostname = req.headers.host; 123 | if (!hostname) { 124 | res.statusCode = 400; 125 | res.end('Host header is required'); 126 | return; 127 | } 128 | 129 | const clientId = GetClientIdFromHostname(hostname); 130 | if (!clientId) { 131 | appCallback(req, res); 132 | return; 133 | } 134 | 135 | const client = manager.getClient(clientId); 136 | if (!client) { 137 | res.statusCode = 404; 138 | res.end('404'); 139 | return; 140 | } 141 | 142 | client.handleRequest(req, res); 143 | }); 144 | 145 | server.on('upgrade', (req, socket, head) => { 146 | const hostname = req.headers.host; 147 | if (!hostname) { 148 | socket.destroy(); 149 | return; 150 | } 151 | 152 | const clientId = GetClientIdFromHostname(hostname); 153 | if (!clientId) { 154 | socket.destroy(); 155 | return; 156 | } 157 | 158 | const client = manager.getClient(clientId); 159 | if (!client) { 160 | socket.destroy(); 161 | return; 162 | } 163 | 164 | client.handleUpgrade(req, socket); 165 | }); 166 | 167 | return server; 168 | }; 169 | -------------------------------------------------------------------------------- /server.test.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import assert from 'assert'; 3 | import { Server as WebSocketServer } from 'ws'; 4 | import WebSocket from 'ws'; 5 | import net from 'net'; 6 | 7 | import createServer from './server'; 8 | 9 | describe('Server', () => { 10 | it('server starts and stops', async () => { 11 | const server = createServer(); 12 | await new Promise(resolve => server.listen(resolve)); 13 | await new Promise(resolve => server.close(resolve)); 14 | }); 15 | 16 | it('should redirect root requests to landing page', async () => { 17 | const server = createServer(); 18 | const res = await request(server).get('/'); 19 | assert.equal('https://localtunnel.github.io/www/', res.headers.location); 20 | }); 21 | 22 | it('should support custom base domains', async () => { 23 | const server = createServer({ 24 | domain: 'domain.example.com', 25 | }); 26 | 27 | const res = await request(server).get('/'); 28 | assert.equal('https://localtunnel.github.io/www/', res.headers.location); 29 | }); 30 | 31 | it('reject long domain name requests', async () => { 32 | const server = createServer(); 33 | const res = await request(server).get('/thisdomainisoutsidethesizeofwhatweallowwhichissixtythreecharacters'); 34 | assert.equal(res.body.message, 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'); 35 | }); 36 | 37 | it('should upgrade websocket requests', async () => { 38 | const hostname = 'websocket-test'; 39 | const server = createServer({ 40 | domain: 'example.com', 41 | }); 42 | await new Promise(resolve => server.listen(resolve)); 43 | 44 | const res = await request(server).get('/websocket-test'); 45 | const localTunnelPort = res.body.port; 46 | 47 | const wss = await new Promise((resolve) => { 48 | const wsServer = new WebSocketServer({ port: 0 }, () => { 49 | resolve(wsServer); 50 | }); 51 | }); 52 | 53 | const websocketServerPort = wss.address().port; 54 | 55 | const ltSocket = net.createConnection({ port: localTunnelPort }); 56 | const wsSocket = net.createConnection({ port: websocketServerPort }); 57 | ltSocket.pipe(wsSocket).pipe(ltSocket); 58 | 59 | wss.once('connection', (ws) => { 60 | ws.once('message', (message) => { 61 | ws.send(message); 62 | }); 63 | }); 64 | 65 | const ws = new WebSocket('http://localhost:' + server.address().port, { 66 | headers: { 67 | host: hostname + '.example.com', 68 | } 69 | }); 70 | 71 | ws.on('open', () => { 72 | ws.send('something'); 73 | }); 74 | 75 | await new Promise((resolve) => { 76 | ws.once('message', (msg) => { 77 | assert.equal(msg, 'something'); 78 | resolve(); 79 | }); 80 | }); 81 | 82 | wss.close(); 83 | await new Promise(resolve => server.close(resolve)); 84 | }); 85 | 86 | it('should support the /api/tunnels/:id/status endpoint', async () => { 87 | const server = createServer(); 88 | await new Promise(resolve => server.listen(resolve)); 89 | 90 | // no such tunnel yet 91 | const res = await request(server).get('/api/tunnels/foobar-test/status'); 92 | assert.equal(res.statusCode, 404); 93 | 94 | // request a new client called foobar-test 95 | { 96 | const res = await request(server).get('/foobar-test'); 97 | } 98 | 99 | { 100 | const res = await request(server).get('/api/tunnels/foobar-test/status'); 101 | assert.equal(res.statusCode, 200); 102 | assert.deepEqual(res.body, { 103 | connected_sockets: 0, 104 | }); 105 | } 106 | 107 | await new Promise(resolve => server.close(resolve)); 108 | }); 109 | }); -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@^1.2.2: 6 | version "1.3.5" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" 8 | dependencies: 9 | mime-types "~2.1.18" 10 | negotiator "0.6.1" 11 | 12 | ansi-regex@^2.0.0: 13 | version "2.1.1" 14 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 15 | 16 | ansi-styles@^2.2.1: 17 | version "2.2.1" 18 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" 19 | 20 | ansicolors@~0.2.1: 21 | version "0.2.1" 22 | resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef" 23 | 24 | any-promise@^1.1.0: 25 | version "1.3.0" 26 | resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" 27 | 28 | array-find-index@^1.0.1: 29 | version "1.0.2" 30 | resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" 31 | 32 | async-limiter@~1.0.0: 33 | version "1.0.0" 34 | resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" 35 | 36 | asynckit@^0.4.0: 37 | version "0.4.0" 38 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 39 | 40 | balanced-match@^1.0.0: 41 | version "1.0.0" 42 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 43 | 44 | book@1.3.3: 45 | version "1.3.3" 46 | resolved "https://registry.yarnpkg.com/book/-/book-1.3.3.tgz#53654254bfe083db102e461544ae9f84be199985" 47 | dependencies: 48 | error-stack-parser "1.3.6" 49 | 50 | brace-expansion@^1.1.7: 51 | version "1.1.11" 52 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 53 | dependencies: 54 | balanced-match "^1.0.0" 55 | concat-map "0.0.1" 56 | 57 | browser-stdout@1.3.1: 58 | version "1.3.1" 59 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" 60 | 61 | builtin-modules@^1.0.0: 62 | version "1.1.1" 63 | resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" 64 | 65 | camelcase-keys@^2.0.0: 66 | version "2.1.0" 67 | resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" 68 | dependencies: 69 | camelcase "^2.0.0" 70 | map-obj "^1.0.0" 71 | 72 | camelcase@^2.0.0: 73 | version "2.1.1" 74 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" 75 | 76 | cardinal@^1.0.0: 77 | version "1.0.0" 78 | resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-1.0.0.tgz#50e21c1b0aa37729f9377def196b5a9cec932ee9" 79 | dependencies: 80 | ansicolors "~0.2.1" 81 | redeyed "~1.0.0" 82 | 83 | chalk@^1.1.3: 84 | version "1.1.3" 85 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" 86 | dependencies: 87 | ansi-styles "^2.2.1" 88 | escape-string-regexp "^1.0.2" 89 | has-ansi "^2.0.0" 90 | strip-ansi "^3.0.0" 91 | supports-color "^2.0.0" 92 | 93 | cli-table@^0.3.1: 94 | version "0.3.1" 95 | resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" 96 | dependencies: 97 | colors "1.0.3" 98 | 99 | cli-usage@^0.1.1: 100 | version "0.1.7" 101 | resolved "https://registry.yarnpkg.com/cli-usage/-/cli-usage-0.1.7.tgz#eaf1c9d5b91e22482333072a12127f05cd99a3ba" 102 | dependencies: 103 | marked "^0.3.12" 104 | marked-terminal "^2.0.0" 105 | 106 | co@^4.6.0: 107 | version "4.6.0" 108 | resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" 109 | 110 | colors@1.0.3: 111 | version "1.0.3" 112 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" 113 | 114 | combined-stream@1.0.6: 115 | version "1.0.6" 116 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" 117 | dependencies: 118 | delayed-stream "~1.0.0" 119 | 120 | commander@2.11.0: 121 | version "2.11.0" 122 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" 123 | 124 | commander@2.5.0: 125 | version "2.5.0" 126 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.5.0.tgz#d777b6a4d847d423e5d475da864294ac1ff5aa9d" 127 | 128 | component-emitter@^1.2.0: 129 | version "1.2.1" 130 | resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" 131 | 132 | concat-map@0.0.1: 133 | version "0.0.1" 134 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 135 | 136 | content-disposition@~0.5.0: 137 | version "0.5.2" 138 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 139 | 140 | content-type@^1.0.0: 141 | version "1.0.4" 142 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 143 | 144 | cookiejar@^2.1.0: 145 | version "2.1.1" 146 | resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a" 147 | 148 | cookies@~0.7.0: 149 | version "0.7.1" 150 | resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.1.tgz#7c8a615f5481c61ab9f16c833731bcb8f663b99b" 151 | dependencies: 152 | depd "~1.1.1" 153 | keygrip "~1.0.2" 154 | 155 | core-util-is@~1.0.0: 156 | version "1.0.2" 157 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 158 | 159 | currently-unhandled@^0.4.1: 160 | version "0.4.1" 161 | resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" 162 | dependencies: 163 | array-find-index "^1.0.1" 164 | 165 | dateformat@~1.0.4-1.2.3: 166 | version "1.0.12" 167 | resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" 168 | dependencies: 169 | get-stdin "^4.0.1" 170 | meow "^3.3.0" 171 | 172 | debounce@^1.0.0: 173 | version "1.1.0" 174 | resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.1.0.tgz#6a1a4ee2a9dc4b7c24bb012558dbcdb05b37f408" 175 | 176 | debug@*, debug@3.1.0, debug@^3.1.0: 177 | version "3.1.0" 178 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 179 | dependencies: 180 | ms "2.0.0" 181 | 182 | decamelize@^1.1.2: 183 | version "1.2.0" 184 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 185 | 186 | deep-equal@~1.0.1: 187 | version "1.0.1" 188 | resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" 189 | 190 | delayed-stream@~1.0.0: 191 | version "1.0.0" 192 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 193 | 194 | delegates@^1.0.0: 195 | version "1.0.0" 196 | resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" 197 | 198 | depd@^1.1.0, depd@~1.1.1, depd@~1.1.2: 199 | version "1.1.2" 200 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 201 | 202 | destroy@^1.0.3: 203 | version "1.0.4" 204 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 205 | 206 | diff@3.5.0: 207 | version "3.5.0" 208 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" 209 | 210 | dynamic-dedupe@^0.2.0: 211 | version "0.2.0" 212 | resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.2.0.tgz#50f7c28684831ecf1c170aab67a1d5311cdd76ce" 213 | dependencies: 214 | xtend "~2.0.6" 215 | 216 | ee-first@1.1.1: 217 | version "1.1.1" 218 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 219 | 220 | end-of-stream@^1.1.0: 221 | version "1.4.1" 222 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" 223 | dependencies: 224 | once "^1.4.0" 225 | 226 | error-ex@^1.2.0: 227 | version "1.3.1" 228 | resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" 229 | dependencies: 230 | is-arrayish "^0.2.1" 231 | 232 | error-inject@~1.0.0: 233 | version "1.0.0" 234 | resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" 235 | 236 | error-stack-parser@1.3.6: 237 | version "1.3.6" 238 | resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-1.3.6.tgz#e0e73b93e417138d1cd7c0b746b1a4a14854c292" 239 | dependencies: 240 | stackframe "^0.3.1" 241 | 242 | escape-html@~1.0.1: 243 | version "1.0.3" 244 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 245 | 246 | escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2: 247 | version "1.0.5" 248 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 249 | 250 | esm@3.0.34: 251 | version "3.0.34" 252 | resolved "https://registry.yarnpkg.com/esm/-/esm-3.0.34.tgz#f86afa35c83a0f535da01d15e625e45794ebddc8" 253 | 254 | esprima@~3.0.0: 255 | version "3.0.0" 256 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9" 257 | 258 | extend@^3.0.0: 259 | version "3.0.1" 260 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" 261 | 262 | filewatcher@~3.0.0: 263 | version "3.0.1" 264 | resolved "https://registry.yarnpkg.com/filewatcher/-/filewatcher-3.0.1.tgz#f4a1957355ddaf443ccd78a895f3d55e23c8a034" 265 | dependencies: 266 | debounce "^1.0.0" 267 | 268 | find-up@^1.0.0: 269 | version "1.1.2" 270 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" 271 | dependencies: 272 | path-exists "^2.0.0" 273 | pinkie-promise "^2.0.0" 274 | 275 | foreach@~2.0.1: 276 | version "2.0.5" 277 | resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" 278 | 279 | form-data@^2.3.1: 280 | version "2.3.2" 281 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" 282 | dependencies: 283 | asynckit "^0.4.0" 284 | combined-stream "1.0.6" 285 | mime-types "^2.1.12" 286 | 287 | formidable@^1.1.1: 288 | version "1.2.1" 289 | resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" 290 | 291 | fresh@^0.5.2: 292 | version "0.5.2" 293 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 294 | 295 | fs.realpath@^1.0.0: 296 | version "1.0.0" 297 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 298 | 299 | get-stdin@^4.0.1: 300 | version "4.0.1" 301 | resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" 302 | 303 | glob@7.1.2: 304 | version "7.1.2" 305 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 306 | dependencies: 307 | fs.realpath "^1.0.0" 308 | inflight "^1.0.4" 309 | inherits "2" 310 | minimatch "^3.0.4" 311 | once "^1.3.0" 312 | path-is-absolute "^1.0.0" 313 | 314 | graceful-fs@^4.1.2: 315 | version "4.1.11" 316 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 317 | 318 | growl@1.10.3: 319 | version "1.10.3" 320 | resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" 321 | 322 | growly@^1.2.0: 323 | version "1.3.0" 324 | resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" 325 | 326 | has-ansi@^2.0.0: 327 | version "2.0.0" 328 | resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" 329 | dependencies: 330 | ansi-regex "^2.0.0" 331 | 332 | has-flag@^2.0.0: 333 | version "2.0.0" 334 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" 335 | 336 | he@1.1.1: 337 | version "1.1.1" 338 | resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" 339 | 340 | hosted-git-info@^2.1.4: 341 | version "2.6.0" 342 | resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222" 343 | 344 | http-assert@^1.1.0: 345 | version "1.3.0" 346 | resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.3.0.tgz#a31a5cf88c873ecbb5796907d4d6f132e8c01e4a" 347 | dependencies: 348 | deep-equal "~1.0.1" 349 | http-errors "~1.6.1" 350 | 351 | http-errors@^1.2.8, http-errors@^1.3.1, http-errors@~1.6.1: 352 | version "1.6.3" 353 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" 354 | dependencies: 355 | depd "~1.1.2" 356 | inherits "2.0.3" 357 | setprototypeof "1.1.0" 358 | statuses ">= 1.4.0 < 2" 359 | 360 | human-readable-ids@1.0.3: 361 | version "1.0.3" 362 | resolved "https://registry.yarnpkg.com/human-readable-ids/-/human-readable-ids-1.0.3.tgz#c8c6c6e95085ccb668087b7dd767834e26ca26d4" 363 | dependencies: 364 | knuth-shuffle "^1.0.0" 365 | 366 | indent-string@^2.1.0: 367 | version "2.1.0" 368 | resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" 369 | dependencies: 370 | repeating "^2.0.0" 371 | 372 | indexof@~0.0.1: 373 | version "0.0.1" 374 | resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" 375 | 376 | inflight@^1.0.4: 377 | version "1.0.6" 378 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 379 | dependencies: 380 | once "^1.3.0" 381 | wrappy "1" 382 | 383 | inherits@2, inherits@2.0.3, inherits@~2.0.3: 384 | version "2.0.3" 385 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 386 | 387 | is-arrayish@^0.2.1: 388 | version "0.2.1" 389 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" 390 | 391 | is-builtin-module@^1.0.0: 392 | version "1.0.0" 393 | resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" 394 | dependencies: 395 | builtin-modules "^1.0.0" 396 | 397 | is-finite@^1.0.0: 398 | version "1.0.2" 399 | resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" 400 | dependencies: 401 | number-is-nan "^1.0.0" 402 | 403 | is-generator-function@^1.0.3: 404 | version "1.0.7" 405 | resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" 406 | 407 | is-object@~0.1.2: 408 | version "0.1.2" 409 | resolved "https://registry.yarnpkg.com/is-object/-/is-object-0.1.2.tgz#00efbc08816c33cfc4ac8251d132e10dc65098d7" 410 | 411 | is-utf8@^0.2.0: 412 | version "0.2.1" 413 | resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" 414 | 415 | is@~0.2.6: 416 | version "0.2.7" 417 | resolved "https://registry.yarnpkg.com/is/-/is-0.2.7.tgz#3b34a2c48f359972f35042849193ae7264b63562" 418 | 419 | isarray@0.0.1: 420 | version "0.0.1" 421 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" 422 | 423 | isarray@~1.0.0: 424 | version "1.0.0" 425 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 426 | 427 | isexe@^2.0.0: 428 | version "2.0.0" 429 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 430 | 431 | keygrip@~1.0.2: 432 | version "1.0.2" 433 | resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91" 434 | 435 | knuth-shuffle@^1.0.0: 436 | version "1.0.8" 437 | resolved "https://registry.yarnpkg.com/knuth-shuffle/-/knuth-shuffle-1.0.8.tgz#929a467b0efd8d297bdcf318ca988a9f1037f80d" 438 | 439 | koa-compose@^3.0.0: 440 | version "3.2.1" 441 | resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" 442 | dependencies: 443 | any-promise "^1.1.0" 444 | 445 | koa-compose@^4.0.0: 446 | version "4.0.0" 447 | resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.0.0.tgz#2800a513d9c361ef0d63852b038e4f6f2d5a773c" 448 | 449 | koa-convert@^1.2.0: 450 | version "1.2.0" 451 | resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" 452 | dependencies: 453 | co "^4.6.0" 454 | koa-compose "^3.0.0" 455 | 456 | koa-is-json@^1.0.0: 457 | version "1.0.0" 458 | resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" 459 | 460 | koa-router@7.4.0: 461 | version "7.4.0" 462 | resolved "https://registry.yarnpkg.com/koa-router/-/koa-router-7.4.0.tgz#aee1f7adc02d5cb31d7d67465c9eacc825e8c5e0" 463 | dependencies: 464 | debug "^3.1.0" 465 | http-errors "^1.3.1" 466 | koa-compose "^3.0.0" 467 | methods "^1.0.1" 468 | path-to-regexp "^1.1.1" 469 | urijs "^1.19.0" 470 | 471 | koa@2.5.1: 472 | version "2.5.1" 473 | resolved "https://registry.yarnpkg.com/koa/-/koa-2.5.1.tgz#79f8b95f8d72d04fe9a58a8da5ebd6d341103f9c" 474 | dependencies: 475 | accepts "^1.2.2" 476 | content-disposition "~0.5.0" 477 | content-type "^1.0.0" 478 | cookies "~0.7.0" 479 | debug "*" 480 | delegates "^1.0.0" 481 | depd "^1.1.0" 482 | destroy "^1.0.3" 483 | error-inject "~1.0.0" 484 | escape-html "~1.0.1" 485 | fresh "^0.5.2" 486 | http-assert "^1.1.0" 487 | http-errors "^1.2.8" 488 | is-generator-function "^1.0.3" 489 | koa-compose "^4.0.0" 490 | koa-convert "^1.2.0" 491 | koa-is-json "^1.0.0" 492 | mime-types "^2.0.7" 493 | on-finished "^2.1.0" 494 | only "0.0.2" 495 | parseurl "^1.3.0" 496 | statuses "^1.2.0" 497 | type-is "^1.5.5" 498 | vary "^1.0.0" 499 | 500 | load-json-file@^1.0.0: 501 | version "1.1.0" 502 | resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" 503 | dependencies: 504 | graceful-fs "^4.1.2" 505 | parse-json "^2.2.0" 506 | pify "^2.0.0" 507 | pinkie-promise "^2.0.0" 508 | strip-bom "^2.0.0" 509 | 510 | localenv@0.2.2: 511 | version "0.2.2" 512 | resolved "https://registry.yarnpkg.com/localenv/-/localenv-0.2.2.tgz#c508f29d3485bdc9341d3ead17f61c5abd1b0bab" 513 | dependencies: 514 | commander "2.5.0" 515 | 516 | lodash._arraycopy@^3.0.0: 517 | version "3.0.0" 518 | resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" 519 | 520 | lodash._arrayeach@^3.0.0: 521 | version "3.0.0" 522 | resolved "https://registry.yarnpkg.com/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz#bab156b2a90d3f1bbd5c653403349e5e5933ef9e" 523 | 524 | lodash._baseassign@^3.0.0: 525 | version "3.2.0" 526 | resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" 527 | dependencies: 528 | lodash._basecopy "^3.0.0" 529 | lodash.keys "^3.0.0" 530 | 531 | lodash._baseclone@^3.0.0: 532 | version "3.3.0" 533 | resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz#303519bf6393fe7e42f34d8b630ef7794e3542b7" 534 | dependencies: 535 | lodash._arraycopy "^3.0.0" 536 | lodash._arrayeach "^3.0.0" 537 | lodash._baseassign "^3.0.0" 538 | lodash._basefor "^3.0.0" 539 | lodash.isarray "^3.0.0" 540 | lodash.keys "^3.0.0" 541 | 542 | lodash._basecopy@^3.0.0: 543 | version "3.0.1" 544 | resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" 545 | 546 | lodash._basefor@^3.0.0: 547 | version "3.0.3" 548 | resolved "https://registry.yarnpkg.com/lodash._basefor/-/lodash._basefor-3.0.3.tgz#7550b4e9218ef09fad24343b612021c79b4c20c2" 549 | 550 | lodash._bindcallback@^3.0.0: 551 | version "3.0.1" 552 | resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" 553 | 554 | lodash._getnative@^3.0.0: 555 | version "3.9.1" 556 | resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" 557 | 558 | lodash.assign@^4.2.0: 559 | version "4.2.0" 560 | resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" 561 | 562 | lodash.clonedeep@^3.0.0: 563 | version "3.0.2" 564 | resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz#a0a1e40d82a5ea89ff5b147b8444ed63d92827db" 565 | dependencies: 566 | lodash._baseclone "^3.0.0" 567 | lodash._bindcallback "^3.0.0" 568 | 569 | lodash.isarguments@^3.0.0: 570 | version "3.1.0" 571 | resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" 572 | 573 | lodash.isarray@^3.0.0: 574 | version "3.0.4" 575 | resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" 576 | 577 | lodash.keys@^3.0.0: 578 | version "3.1.2" 579 | resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" 580 | dependencies: 581 | lodash._getnative "^3.0.0" 582 | lodash.isarguments "^3.0.0" 583 | lodash.isarray "^3.0.0" 584 | 585 | lodash.toarray@^4.4.0: 586 | version "4.4.0" 587 | resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" 588 | 589 | loud-rejection@^1.0.0: 590 | version "1.6.0" 591 | resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" 592 | dependencies: 593 | currently-unhandled "^0.4.1" 594 | signal-exit "^3.0.0" 595 | 596 | map-obj@^1.0.0, map-obj@^1.0.1: 597 | version "1.0.1" 598 | resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" 599 | 600 | marked-terminal@^2.0.0: 601 | version "2.0.0" 602 | resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-2.0.0.tgz#5eaf568be66f686541afa52a558280310a31de2d" 603 | dependencies: 604 | cardinal "^1.0.0" 605 | chalk "^1.1.3" 606 | cli-table "^0.3.1" 607 | lodash.assign "^4.2.0" 608 | node-emoji "^1.4.1" 609 | 610 | marked@^0.3.12: 611 | version "0.3.19" 612 | resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790" 613 | 614 | media-typer@0.3.0: 615 | version "0.3.0" 616 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 617 | 618 | meow@^3.3.0: 619 | version "3.7.0" 620 | resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" 621 | dependencies: 622 | camelcase-keys "^2.0.0" 623 | decamelize "^1.1.2" 624 | loud-rejection "^1.0.0" 625 | map-obj "^1.0.1" 626 | minimist "^1.1.3" 627 | normalize-package-data "^2.3.4" 628 | object-assign "^4.0.1" 629 | read-pkg-up "^1.0.1" 630 | redent "^1.0.0" 631 | trim-newlines "^1.0.0" 632 | 633 | methods@^1.0.1, methods@^1.1.1, methods@~1.1.2: 634 | version "1.1.2" 635 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 636 | 637 | mime-db@~1.33.0: 638 | version "1.33.0" 639 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" 640 | 641 | mime-types@^2.0.7, mime-types@^2.1.12, mime-types@~2.1.18: 642 | version "2.1.18" 643 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" 644 | dependencies: 645 | mime-db "~1.33.0" 646 | 647 | mime@^1.4.1: 648 | version "1.6.0" 649 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 650 | 651 | minimatch@3.0.4, minimatch@^3.0.4: 652 | version "3.0.4" 653 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 654 | dependencies: 655 | brace-expansion "^1.1.7" 656 | 657 | minimist@0.0.8: 658 | version "0.0.8" 659 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 660 | 661 | minimist@^1.1.1, minimist@^1.1.3: 662 | version "1.2.0" 663 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 664 | 665 | minimist@~0.0.1: 666 | version "0.0.10" 667 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" 668 | 669 | mkdirp@0.5.1: 670 | version "0.5.1" 671 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 672 | dependencies: 673 | minimist "0.0.8" 674 | 675 | mocha@5.1.1: 676 | version "5.1.1" 677 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.1.1.tgz#b774c75609dac05eb48f4d9ba1d827b97fde8a7b" 678 | dependencies: 679 | browser-stdout "1.3.1" 680 | commander "2.11.0" 681 | debug "3.1.0" 682 | diff "3.5.0" 683 | escape-string-regexp "1.0.5" 684 | glob "7.1.2" 685 | growl "1.10.3" 686 | he "1.1.1" 687 | minimatch "3.0.4" 688 | mkdirp "0.5.1" 689 | supports-color "4.4.0" 690 | 691 | ms@2.0.0: 692 | version "2.0.0" 693 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 694 | 695 | negotiator@0.6.1: 696 | version "0.6.1" 697 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 698 | 699 | node-dev@3.1.3: 700 | version "3.1.3" 701 | resolved "https://registry.yarnpkg.com/node-dev/-/node-dev-3.1.3.tgz#582719223ebdef5d63059e6a7fbcd2399fc0f84d" 702 | dependencies: 703 | dateformat "~1.0.4-1.2.3" 704 | dynamic-dedupe "^0.2.0" 705 | filewatcher "~3.0.0" 706 | minimist "^1.1.3" 707 | node-notifier "^4.0.2" 708 | resolve "^1.0.0" 709 | 710 | node-emoji@^1.4.1: 711 | version "1.8.1" 712 | resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.8.1.tgz#6eec6bfb07421e2148c75c6bba72421f8530a826" 713 | dependencies: 714 | lodash.toarray "^4.4.0" 715 | 716 | node-notifier@^4.0.2: 717 | version "4.6.1" 718 | resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-4.6.1.tgz#056d14244f3dcc1ceadfe68af9cff0c5473a33f3" 719 | dependencies: 720 | cli-usage "^0.1.1" 721 | growly "^1.2.0" 722 | lodash.clonedeep "^3.0.0" 723 | minimist "^1.1.1" 724 | semver "^5.1.0" 725 | shellwords "^0.1.0" 726 | which "^1.0.5" 727 | 728 | normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: 729 | version "2.4.0" 730 | resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" 731 | dependencies: 732 | hosted-git-info "^2.1.4" 733 | is-builtin-module "^1.0.0" 734 | semver "2 || 3 || 4 || 5" 735 | validate-npm-package-license "^3.0.1" 736 | 737 | number-is-nan@^1.0.0: 738 | version "1.0.1" 739 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 740 | 741 | object-assign@^4.0.1: 742 | version "4.1.1" 743 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 744 | 745 | object-keys@~0.2.0: 746 | version "0.2.0" 747 | resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.2.0.tgz#cddec02998b091be42bf1035ae32e49f1cb6ea67" 748 | dependencies: 749 | foreach "~2.0.1" 750 | indexof "~0.0.1" 751 | is "~0.2.6" 752 | 753 | on-finished@^2.1.0: 754 | version "2.3.0" 755 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 756 | dependencies: 757 | ee-first "1.1.1" 758 | 759 | once@^1.3.0, once@^1.3.1, once@^1.4.0: 760 | version "1.4.0" 761 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 762 | dependencies: 763 | wrappy "1" 764 | 765 | only@0.0.2: 766 | version "0.0.2" 767 | resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" 768 | 769 | optimist@0.6.1: 770 | version "0.6.1" 771 | resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" 772 | dependencies: 773 | minimist "~0.0.1" 774 | wordwrap "~0.0.2" 775 | 776 | parse-json@^2.2.0: 777 | version "2.2.0" 778 | resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" 779 | dependencies: 780 | error-ex "^1.2.0" 781 | 782 | parseurl@^1.3.0: 783 | version "1.3.2" 784 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" 785 | 786 | path-exists@^2.0.0: 787 | version "2.1.0" 788 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" 789 | dependencies: 790 | pinkie-promise "^2.0.0" 791 | 792 | path-is-absolute@^1.0.0: 793 | version "1.0.1" 794 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 795 | 796 | path-parse@^1.0.5: 797 | version "1.0.5" 798 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" 799 | 800 | path-to-regexp@^1.1.1: 801 | version "1.7.0" 802 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" 803 | dependencies: 804 | isarray "0.0.1" 805 | 806 | path-type@^1.0.0: 807 | version "1.1.0" 808 | resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" 809 | dependencies: 810 | graceful-fs "^4.1.2" 811 | pify "^2.0.0" 812 | pinkie-promise "^2.0.0" 813 | 814 | pify@^2.0.0: 815 | version "2.3.0" 816 | resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" 817 | 818 | pinkie-promise@^2.0.0: 819 | version "2.0.1" 820 | resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" 821 | dependencies: 822 | pinkie "^2.0.0" 823 | 824 | pinkie@^2.0.0: 825 | version "2.0.4" 826 | resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" 827 | 828 | process-nextick-args@~2.0.0: 829 | version "2.0.0" 830 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" 831 | 832 | pump@3.0.0: 833 | version "3.0.0" 834 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" 835 | dependencies: 836 | end-of-stream "^1.1.0" 837 | once "^1.3.1" 838 | 839 | punycode@^1.4.1: 840 | version "1.4.1" 841 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" 842 | 843 | qs@^6.5.1: 844 | version "6.5.2" 845 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" 846 | 847 | read-pkg-up@^1.0.1: 848 | version "1.0.1" 849 | resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" 850 | dependencies: 851 | find-up "^1.0.0" 852 | read-pkg "^1.0.0" 853 | 854 | read-pkg@^1.0.0: 855 | version "1.1.0" 856 | resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" 857 | dependencies: 858 | load-json-file "^1.0.0" 859 | normalize-package-data "^2.3.2" 860 | path-type "^1.0.0" 861 | 862 | readable-stream@^2.0.5: 863 | version "2.3.6" 864 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" 865 | dependencies: 866 | core-util-is "~1.0.0" 867 | inherits "~2.0.3" 868 | isarray "~1.0.0" 869 | process-nextick-args "~2.0.0" 870 | safe-buffer "~5.1.1" 871 | string_decoder "~1.1.1" 872 | util-deprecate "~1.0.1" 873 | 874 | redent@^1.0.0: 875 | version "1.0.0" 876 | resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" 877 | dependencies: 878 | indent-string "^2.1.0" 879 | strip-indent "^1.0.1" 880 | 881 | redeyed@~1.0.0: 882 | version "1.0.1" 883 | resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-1.0.1.tgz#e96c193b40c0816b00aec842698e61185e55498a" 884 | dependencies: 885 | esprima "~3.0.0" 886 | 887 | repeating@^2.0.0: 888 | version "2.0.1" 889 | resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" 890 | dependencies: 891 | is-finite "^1.0.0" 892 | 893 | resolve@^1.0.0: 894 | version "1.7.1" 895 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" 896 | dependencies: 897 | path-parse "^1.0.5" 898 | 899 | safe-buffer@~5.1.0, safe-buffer@~5.1.1: 900 | version "5.1.2" 901 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 902 | 903 | "semver@2 || 3 || 4 || 5", semver@^5.1.0: 904 | version "5.5.0" 905 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" 906 | 907 | setprototypeof@1.1.0: 908 | version "1.1.0" 909 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" 910 | 911 | shellwords@^0.1.0: 912 | version "0.1.1" 913 | resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" 914 | 915 | signal-exit@^3.0.0: 916 | version "3.0.2" 917 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 918 | 919 | spdx-correct@^3.0.0: 920 | version "3.0.0" 921 | resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" 922 | dependencies: 923 | spdx-expression-parse "^3.0.0" 924 | spdx-license-ids "^3.0.0" 925 | 926 | spdx-exceptions@^2.1.0: 927 | version "2.1.0" 928 | resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" 929 | 930 | spdx-expression-parse@^3.0.0: 931 | version "3.0.0" 932 | resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" 933 | dependencies: 934 | spdx-exceptions "^2.1.0" 935 | spdx-license-ids "^3.0.0" 936 | 937 | spdx-license-ids@^3.0.0: 938 | version "3.0.0" 939 | resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" 940 | 941 | stackframe@^0.3.1: 942 | version "0.3.1" 943 | resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-0.3.1.tgz#33aa84f1177a5548c8935533cbfeb3420975f5a4" 944 | 945 | "statuses@>= 1.4.0 < 2", statuses@^1.2.0: 946 | version "1.5.0" 947 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 948 | 949 | string_decoder@~1.1.1: 950 | version "1.1.1" 951 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" 952 | dependencies: 953 | safe-buffer "~5.1.0" 954 | 955 | strip-ansi@^3.0.0: 956 | version "3.0.1" 957 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 958 | dependencies: 959 | ansi-regex "^2.0.0" 960 | 961 | strip-bom@^2.0.0: 962 | version "2.0.0" 963 | resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" 964 | dependencies: 965 | is-utf8 "^0.2.0" 966 | 967 | strip-indent@^1.0.1: 968 | version "1.0.1" 969 | resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" 970 | dependencies: 971 | get-stdin "^4.0.1" 972 | 973 | superagent@3.8.2: 974 | version "3.8.2" 975 | resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403" 976 | dependencies: 977 | component-emitter "^1.2.0" 978 | cookiejar "^2.1.0" 979 | debug "^3.1.0" 980 | extend "^3.0.0" 981 | form-data "^2.3.1" 982 | formidable "^1.1.1" 983 | methods "^1.1.1" 984 | mime "^1.4.1" 985 | qs "^6.5.1" 986 | readable-stream "^2.0.5" 987 | 988 | supertest@3.1.0: 989 | version "3.1.0" 990 | resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.1.0.tgz#f9ebaf488e60f2176021ec580bdd23ad269e7bc6" 991 | dependencies: 992 | methods "~1.1.2" 993 | superagent "3.8.2" 994 | 995 | supports-color@4.4.0: 996 | version "4.4.0" 997 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" 998 | dependencies: 999 | has-flag "^2.0.0" 1000 | 1001 | supports-color@^2.0.0: 1002 | version "2.0.0" 1003 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" 1004 | 1005 | tldjs@2.3.1: 1006 | version "2.3.1" 1007 | resolved "https://registry.yarnpkg.com/tldjs/-/tldjs-2.3.1.tgz#cf09c3eb5d7403a9e214b7d65f3cf9651c0ab039" 1008 | dependencies: 1009 | punycode "^1.4.1" 1010 | 1011 | trim-newlines@^1.0.0: 1012 | version "1.0.0" 1013 | resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" 1014 | 1015 | type-is@^1.5.5: 1016 | version "1.6.16" 1017 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" 1018 | dependencies: 1019 | media-typer "0.3.0" 1020 | mime-types "~2.1.18" 1021 | 1022 | urijs@^1.19.0: 1023 | version "1.19.1" 1024 | resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a" 1025 | 1026 | util-deprecate@~1.0.1: 1027 | version "1.0.2" 1028 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 1029 | 1030 | validate-npm-package-license@^3.0.1: 1031 | version "3.0.3" 1032 | resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" 1033 | dependencies: 1034 | spdx-correct "^3.0.0" 1035 | spdx-expression-parse "^3.0.0" 1036 | 1037 | vary@^1.0.0: 1038 | version "1.1.2" 1039 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1040 | 1041 | which@^1.0.5: 1042 | version "1.3.0" 1043 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" 1044 | dependencies: 1045 | isexe "^2.0.0" 1046 | 1047 | wordwrap@~0.0.2: 1048 | version "0.0.3" 1049 | resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" 1050 | 1051 | wrappy@1: 1052 | version "1.0.2" 1053 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1054 | 1055 | ws@5.1.1: 1056 | version "5.1.1" 1057 | resolved "https://registry.yarnpkg.com/ws/-/ws-5.1.1.tgz#1d43704689711ac1942fd2f283e38f825c4b8b95" 1058 | dependencies: 1059 | async-limiter "~1.0.0" 1060 | 1061 | xtend@~2.0.6: 1062 | version "2.0.6" 1063 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.0.6.tgz#5ea657a6dba447069c2e59c58a1138cb0c5e6cee" 1064 | dependencies: 1065 | is-object "~0.1.2" 1066 | object-keys "~0.2.0" 1067 | --------------------------------------------------------------------------------