diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 1dab4ed..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -save-exact = true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f94a53e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: node_js -sudo: false -node_js: - - "9.2" diff --git a/Dockerfile b/Dockerfile index ddda30b..3e0d6cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,15 @@ -FROM node:10.1.0-alpine +FROM oven/bun:1 as base WORKDIR /app COPY package.json /app/ -COPY yarn.lock /app/ +COPY bun.lockb /app/ -RUN yarn install --production && yarn cache clean +RUN bun install --frozen-lockfile --production COPY . /app ENV NODE_ENV production -ENTRYPOINT ["node", "-r", "esm", "./bin/server"] +ENTRYPOINT ["bun", "run", "server"] + +EXPOSE 1234 \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 9c829f0..0000000 --- a/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# localtunnel-server - -[![Build Status](https://travis-ci.org/localtunnel/server.svg?branch=master)](https://travis-ci.org/localtunnel/server) - -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. - -This repo is the server component. If you are just looking for the CLI localtunnel app, see (https://github.com/localtunnel/localtunnel). - -## overview ## - -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: - -* You can set up DNS entries for your `domain.tld` and `*.domain.tld` (or `sub.domain.tld` and `*.sub.domain.tld`). -* The server can accept incoming TCP connections for any non-root TCP port (i.e. ports over 1000). - -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. - -#### setup - -```shell -# pick a place where the files will live -git clone git://github.com/defunctzombie/localtunnel-server.git -cd localtunnel-server -npm install - -# server set to run on port 1234 -bin/server --port 1234 -``` - -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). - -**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) - -#### use your server - -You can now use your domain with the `--host` flag for the `lt` client. - -```shell -lt --host http://sub.example.tld:1234 --port 9000 -``` - -You will be assigned a URL similar to `heavy-puma-9.sub.example.com:1234`. - -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. - -## REST API - -### POST /api/tunnels - -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. - -### GET /api/status - -General server information. - -## Deploy - -You can deploy your own localtunnel server using the prebuilt docker image. - -**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. - -If you do not want ssl support for your own tunnel (not recommended), then you can just run the below with `--port 80` instead. - -``` -docker run -d \ - --restart always \ - --name localtunnel \ - --net host \ - defunctzombie/localtunnel-server:latest --port 3000 -``` diff --git a/bin/server b/bin/server deleted file mode 100755 index a6bba9d..0000000 --- a/bin/server +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env node -r esm - -import 'localenv'; -import optimist from 'optimist'; - -import log from 'book'; -import Debug from 'debug'; - -import CreateServer from '../server'; - -const debug = Debug('localtunnel'); - -const argv = optimist - .usage('Usage: $0 --port [num]') - .options('secure', { - default: false, - describe: 'use this flag to indicate proxy over https' - }) - .options('port', { - default: '80', - describe: 'listen on this port for outside requests' - }) - .options('address', { - default: '0.0.0.0', - describe: 'IP address to bind to' - }) - .options('domain', { - 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)', - }) - .options('max-sockets', { - default: 10, - describe: 'maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)' - }) - .argv; - -if (argv.help) { - optimist.showHelp(); - process.exit(); -} - -const server = CreateServer({ - max_tcp_sockets: argv['max-sockets'], - secure: argv.secure, - domain: argv.domain, -}); - -server.listen(argv.port, argv.address, () => { - debug('server listening on port: %d', server.address().port); -}); - -process.on('SIGINT', () => { - process.exit(); -}); - -process.on('SIGTERM', () => { - process.exit(); -}); - -process.on('uncaughtException', (err) => { - log.error(err); -}); - -process.on('unhandledRejection', (reason, promise) => { - log.error(reason); -}); - -// vim: ft=javascript - diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..ea105d1 Binary files /dev/null and b/bun.lockb differ diff --git a/client.ts b/client.ts new file mode 100644 index 0000000..1b7d624 --- /dev/null +++ b/client.ts @@ -0,0 +1,50 @@ +import { parseArgs } from "util"; + +async function main({ url, domain, subdomain }: { url: string; domain?: string, subdomain?: string }) { + const serverUrl = `ws://${domain || "localhost:1234"}?new${subdomain ? `&subdomain=${subdomain}` : ""}`; + const socket = new WebSocket(serverUrl); + + socket.addEventListener("message", (event) => { + const data = JSON.parse(event.data); + console.log("message:", data); + + if (data.method) { + fetch(`${url}${data.path}`, { + method: data.method, + headers: data.headers, + }) + .then((res) => res.text()) + .then((res) => { + socket.send(res); + }); + } + }); + + socket.addEventListener("open", (event) => { + console.log("socket ready:", !!(event.target as any).readyState); + socket.ping(); + }); +} + +const { values } = parseArgs({ + args: Bun.argv, + options: { + port: { + type: "string", + required: true, + short: "p", + }, + domain: { + type: "string", + short: "d", + }, + subdomain: { + type: "string", + short: "s", + }, + }, + allowPositionals: true, +}); + +if (!values.port) throw "pass --port 3000"; +main({ url: `localhost:${values.port}`, domain: values.domain, subdomain: values.subdomain }); diff --git a/demo.ts b/demo.ts new file mode 100644 index 0000000..fc9a8d2 --- /dev/null +++ b/demo.ts @@ -0,0 +1,22 @@ +import { serve } from "bun"; + +const port = 3000; + +serve({ + port, + fetch: async (req) => { + const url = new URL(req.url); + console.log(req.method, url.pathname, url.search); + + const bod = await req.text(); + bod && console.log("Body: ", bod); + + const res = new Response(`hello from localhost:${port}`, { + status: 200, + }); + + return res; + }, +}); + +console.log(`Serving on port ${port}!`); diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..a031bec --- /dev/null +++ b/fly.toml @@ -0,0 +1,20 @@ +# fly.toml app configuration file generated for tunnel on 2024-03-18T21:37:28-04:00 +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. + +app = 'tunnel' +primary_region = 'yul' + +[build] + +[http_service] + internal_port = 1234 + force_https = false + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + +[[vm]] + memory = '1gb' + cpu_kind = 'shared' + cpus = 1 \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100755 index 0000000..fdf5002 --- /dev/null +++ b/index.ts @@ -0,0 +1,67 @@ +import "localenv"; +import optimist from "optimist"; +import Debug from "debug"; +import { CreateServer } from "./server"; +import { type AddressInfo } from "ws"; + +const debug = Debug("localtunnel"); + +const argv = optimist + .usage("Usage: $0 --port [num]") + .options("secure", { + default: false, + describe: "use this flag to indicate proxy over https", + }) + .options("port", { + default: "80", + describe: "listen on this port for outside requests", + }) + .options("address", { + default: "0.0.0.0", + describe: "IP address to bind to", + }) + .options("domain", { + 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)", + }) + .options("max-sockets", { + default: 10, + describe: + "maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)", + }).argv; + +if (argv.help) { + console.log("Showing help"); + optimist.showHelp(); + process.exit(); +} + +const server = CreateServer({ + max_tcp_sockets: argv["max-sockets"], + secure: argv.secure, + domain: argv.domain, +}); + +server.listen(argv.port, argv.address, () => { + debug("server listening on port: %d", (server.address() as AddressInfo).port); +}); + +process.on("SIGINT", () => { + console.error("SIGINT"); + process.exit(); +}); + +process.on("SIGTERM", () => { + console.warn("SIGTERM"); + // process.exit(); +}); + +process.on("uncaughtException", (err) => { + console.error("err:"); + console.error(err); +}); + +process.on("unhandledRejection", (reason, promise) => { + console.error("reason:"); + console.error(reason); +}); diff --git a/lib/Client.js b/lib/Client.js deleted file mode 100644 index 369eb07..0000000 --- a/lib/Client.js +++ /dev/null @@ -1,132 +0,0 @@ -import http from 'http'; -import Debug from 'debug'; -import pump from 'pump'; -import EventEmitter from 'events'; - -// A client encapsulates req/res handling using an agent -// -// If an agent is destroyed, the request handling will error -// The caller is responsible for handling a failed request -class Client extends EventEmitter { - constructor(options) { - super(); - - const agent = this.agent = options.agent; - const id = this.id = options.id; - - this.debug = Debug(`lt:Client[${this.id}]`); - - // client is given a grace period in which they can connect before they are _removed_ - this.graceTimeout = setTimeout(() => { - this.close(); - }, 1000).unref(); - - agent.on('online', () => { - this.debug('client online %s', id); - clearTimeout(this.graceTimeout); - }); - - agent.on('offline', () => { - this.debug('client offline %s', id); - - // if there was a previous timeout set, we don't want to double trigger - clearTimeout(this.graceTimeout); - - // client is given a grace period in which they can re-connect before they are _removed_ - this.graceTimeout = setTimeout(() => { - this.close(); - }, 1000).unref(); - }); - - // TODO(roman): an agent error removes the client, the user needs to re-connect? - // how does a user realize they need to re-connect vs some random client being assigned same port? - agent.once('error', (err) => { - this.close(); - }); - } - - stats() { - return this.agent.stats(); - } - - close() { - clearTimeout(this.graceTimeout); - this.agent.destroy(); - this.emit('close'); - } - - handleRequest(req, res) { - this.debug('> %s', req.url); - const opt = { - path: req.url, - agent: this.agent, - method: req.method, - headers: req.headers - }; - - const clientReq = http.request(opt, (clientRes) => { - this.debug('< %s', req.url); - // write response code and headers - res.writeHead(clientRes.statusCode, clientRes.headers); - - // using pump is deliberate - see the pump docs for why - pump(clientRes, res); - }); - - // this can happen when underlying agent produces an error - // in our case we 504 gateway error this? - // if we have already sent headers? - clientReq.once('error', (err) => { - // TODO(roman): if headers not sent - respond with gateway unavailable - }); - - // using pump is deliberate - see the pump docs for why - pump(req, clientReq); - } - - handleUpgrade(req, socket) { - this.debug('> [up] %s', req.url); - socket.once('error', (err) => { - // These client side errors can happen if the client dies while we are reading - // We don't need to surface these in our logs. - if (err.code == 'ECONNRESET' || err.code == 'ETIMEDOUT') { - return; - } - console.error(err); - }); - - this.agent.createConnection({}, (err, conn) => { - this.debug('< [up] %s', req.url); - // any errors getting a connection mean we cannot service this request - if (err) { - socket.end(); - return; - } - - // socket met have disconnected while we waiting for a socket - if (!socket.readable || !socket.writable) { - conn.destroy(); - socket.end(); - return; - } - - // websocket requests are special in that we simply re-create the header info - // then directly pipe the socket data - // avoids having to rebuild the request and handle upgrades via the http client - const arr = [`${req.method} ${req.url} HTTP/${req.httpVersion}`]; - for (let i=0 ; i < (req.rawHeaders.length-1) ; i+=2) { - arr.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}`); - } - - arr.push(''); - arr.push(''); - - // using pump is deliberate - see the pump docs for why - pump(conn, socket); - pump(socket, conn); - conn.write(arr.join('\r\n')); - }); - } -} - -export default Client; \ No newline at end of file diff --git a/lib/Client.test.js b/lib/Client.test.js deleted file mode 100644 index 5a0afd0..0000000 --- a/lib/Client.test.js +++ /dev/null @@ -1,157 +0,0 @@ -import assert from 'assert'; -import http from 'http'; -import { Duplex } from 'stream'; -import WebSocket from 'ws'; -import net from 'net'; - -import Client from './Client'; - -class DummySocket extends Duplex { - constructor(options) { - super(options); - } - - _write(chunk, encoding, callback) { - callback(); - } - - _read(size) { - this.push('HTTP/1.1 304 Not Modified\r\nX-Powered-By: dummy\r\n\r\n\r\n'); - this.push(null); - } -} - -class DummyWebsocket extends Duplex { - constructor(options) { - super(options); - this.sentHeader = false; - } - - _write(chunk, encoding, callback) { - const str = chunk.toString(); - // if chunk contains `GET / HTTP/1.1` -> queue headers - // otherwise echo back received data - if (str.indexOf('GET / HTTP/1.1') === 0) { - const arr = [ - 'HTTP/1.1 101 Switching Protocols', - 'Upgrade: websocket', - 'Connection: Upgrade', - ]; - this.push(arr.join('\r\n')); - this.push('\r\n\r\n'); - } - else { - this.push(str); - } - callback(); - } - - _read(size) { - // nothing to implement - } -} - -class DummyAgent extends http.Agent { - constructor() { - super(); - } - - createConnection(options, cb) { - cb(null, new DummySocket()); - } -} - -describe('Client', () => { - it('should handle request', async () => { - const agent = new DummyAgent(); - const client = new Client({ agent }); - - const server = http.createServer((req, res) => { - client.handleRequest(req, res); - }); - - await new Promise(resolve => server.listen(resolve)); - - const address = server.address(); - const opt = { - host: 'localhost', - port: address.port, - path: '/', - }; - - const res = await new Promise((resolve) => { - const req = http.get(opt, (res) => { - resolve(res); - }); - req.end(); - }); - assert.equal(res.headers['x-powered-by'], 'dummy'); - server.close(); - }); - - it('should handle upgrade', async () => { - // need a websocket server and a socket for it - class DummyWebsocketAgent extends http.Agent { - constructor() { - super(); - } - - createConnection(options, cb) { - cb(null, new DummyWebsocket()); - } - } - - const agent = new DummyWebsocketAgent(); - const client = new Client({ agent }); - - const server = http.createServer(); - server.on('upgrade', (req, socket, head) => { - client.handleUpgrade(req, socket); - }); - - await new Promise(resolve => server.listen(resolve)); - - const address = server.address(); - - const netClient = await new Promise((resolve) => { - const newClient = net.createConnection({ port: address.port }, () => { - resolve(newClient); - }); - }); - - const out = [ - 'GET / HTTP/1.1', - 'Connection: Upgrade', - 'Upgrade: websocket' - ]; - - netClient.write(out.join('\r\n') + '\r\n\r\n'); - - { - const data = await new Promise((resolve) => { - netClient.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - const exp = [ - 'HTTP/1.1 101 Switching Protocols', - 'Upgrade: websocket', - 'Connection: Upgrade', - ]; - assert.equal(exp.join('\r\n') + '\r\n\r\n', data); - } - - { - netClient.write('foobar'); - const data = await new Promise((resolve) => { - netClient.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - assert.equal('foobar', data); - } - - netClient.destroy(); - server.close(); - }); -}); diff --git a/lib/ClientManager.js b/lib/ClientManager.js deleted file mode 100644 index e1a7838..0000000 --- a/lib/ClientManager.js +++ /dev/null @@ -1,96 +0,0 @@ -import { hri } from 'human-readable-ids'; -import Debug from 'debug'; - -import Client from './Client'; -import TunnelAgent from './TunnelAgent'; - -// Manage sets of clients -// -// A client is a "user session" established to service a remote localtunnel client -class ClientManager { - constructor(opt) { - this.opt = opt || {}; - - // id -> client instance - this.clients = new Map(); - - // statistics - this.stats = { - tunnels: 0 - }; - - this.debug = Debug('lt:ClientManager'); - - // This is totally wrong :facepalm: this needs to be per-client... - this.graceTimeout = null; - } - - // create a new tunnel with `id` - // if the id is already used, a random id is assigned - // if the tunnel could not be created, throws an error - async newClient(id) { - const clients = this.clients; - const stats = this.stats; - - // can't ask for id already is use - if (clients[id]) { - id = hri.random(); - } - - const maxSockets = this.opt.max_tcp_sockets; - const agent = new TunnelAgent({ - clientId: id, - maxSockets: 10, - }); - - const client = new Client({ - id, - agent, - }); - - // add to clients map immediately - // avoiding races with other clients requesting same id - clients[id] = client; - - client.once('close', () => { - this.removeClient(id); - }); - - // try/catch used here to remove client id - try { - const info = await agent.listen(); - ++stats.tunnels; - return { - id: id, - port: info.port, - max_conn_count: maxSockets, - }; - } - catch (err) { - this.removeClient(id); - // rethrow error for upstream to handle - throw err; - } - } - - removeClient(id) { - this.debug('removing client: %s', id); - const client = this.clients[id]; - if (!client) { - return; - } - --this.stats.tunnels; - delete this.clients[id]; - client.close(); - } - - hasClient(id) { - return !!this.clients[id]; - } - - getClient(id) { - return this.clients[id]; - } -} - -export default ClientManager; diff --git a/lib/ClientManager.test.js b/lib/ClientManager.test.js deleted file mode 100644 index d85abe5..0000000 --- a/lib/ClientManager.test.js +++ /dev/null @@ -1,90 +0,0 @@ -import assert from 'assert'; -import net from 'net'; - -import ClientManager from './ClientManager'; - -describe('ClientManager', () => { - it('should construct with no tunnels', () => { - const manager = new ClientManager(); - assert.equal(manager.stats.tunnels, 0); - }); - - it('should create a new client with random id', async () => { - const manager = new ClientManager(); - const client = await manager.newClient(); - assert(manager.hasClient(client.id)); - manager.removeClient(client.id); - }); - - it('should create a new client with id', async () => { - const manager = new ClientManager(); - const client = await manager.newClient('foobar'); - assert(manager.hasClient('foobar')); - manager.removeClient('foobar'); - }); - - it('should create a new client with random id if previous exists', async () => { - const manager = new ClientManager(); - const clientA = await manager.newClient('foobar'); - const clientB = await manager.newClient('foobar'); - assert(clientA.id, 'foobar'); - assert(manager.hasClient(clientB.id)); - assert(clientB.id != clientA.id); - manager.removeClient(clientB.id); - manager.removeClient('foobar'); - }); - - it('should remove client once it goes offline', async () => { - const manager = new ClientManager(); - const client = await manager.newClient('foobar'); - - const socket = await new Promise((resolve) => { - const netClient = net.createConnection({ port: client.port }, () => { - resolve(netClient); - }); - }); - const closePromise = new Promise(resolve => socket.once('close', resolve)); - socket.end(); - await closePromise; - - // should still have client - grace period has not expired - assert(manager.hasClient('foobar')); - - // wait past grace period (1s) - await new Promise(resolve => setTimeout(resolve, 1500)); - assert(!manager.hasClient('foobar')); - }).timeout(5000); - - it('should remove correct client once it goes offline', async () => { - const manager = new ClientManager(); - const clientFoo = await manager.newClient('foo'); - const clientBar = await manager.newClient('bar'); - - const socket = await new Promise((resolve) => { - const netClient = net.createConnection({ port: clientFoo.port }, () => { - resolve(netClient); - }); - }); - - await new Promise(resolve => setTimeout(resolve, 1500)); - - // foo should still be ok - assert(manager.hasClient('foo')); - - // clientBar shound be removed - nothing connected to it - assert(!manager.hasClient('bar')); - - manager.removeClient('foo'); - socket.end(); - }).timeout(5000); - - it('should remove clients if they do not connect within 5 seconds', async () => { - const manager = new ClientManager(); - const clientFoo = await manager.newClient('foo'); - assert(manager.hasClient('foo')); - - // wait past grace period (1s) - await new Promise(resolve => setTimeout(resolve, 1500)); - assert(!manager.hasClient('foo')); - }).timeout(5000); -}); diff --git a/lib/TunnelAgent.js b/lib/TunnelAgent.js deleted file mode 100644 index efc2231..0000000 --- a/lib/TunnelAgent.js +++ /dev/null @@ -1,175 +0,0 @@ -import { Agent } from 'http'; -import net from 'net'; -import assert from 'assert'; -import log from 'book'; -import Debug from 'debug'; - -const DEFAULT_MAX_SOCKETS = 10; - -// Implements an http.Agent interface to a pool of tunnel sockets -// A tunnel socket is a connection _from_ a client that will -// service http requests. This agent is usable wherever one can use an http.Agent -class TunnelAgent extends Agent { - constructor(options = {}) { - super({ - keepAlive: true, - // only allow keepalive to hold on to one socket - // this prevents it from holding on to all the sockets so they can be used for upgrades - maxFreeSockets: 1, - }); - - // sockets we can hand out via createConnection - this.availableSockets = []; - - // when a createConnection cannot return a socket, it goes into a queue - // once a socket is available it is handed out to the next callback - this.waitingCreateConn = []; - - this.debug = Debug(`lt:TunnelAgent[${options.clientId}]`); - - // track maximum allowed sockets - this.connectedSockets = 0; - this.maxTcpSockets = options.maxTcpSockets || DEFAULT_MAX_SOCKETS; - - // new tcp server to service requests for this client - this.server = net.createServer(); - - // flag to avoid double starts - this.started = false; - this.closed = false; - } - - stats() { - return { - connectedSockets: this.connectedSockets, - }; - } - - listen() { - const server = this.server; - if (this.started) { - throw new Error('already started'); - } - this.started = true; - - server.on('close', this._onClose.bind(this)); - server.on('connection', this._onConnection.bind(this)); - server.on('error', (err) => { - // These errors happen from killed connections, we don't worry about them - if (err.code == 'ECONNRESET' || err.code == 'ETIMEDOUT') { - return; - } - log.error(err); - }); - - return new Promise((resolve) => { - server.listen(() => { - const port = server.address().port; - this.debug('tcp server listening on port: %d', port); - - resolve({ - // port for lt client tcp connections - port: port, - }); - }); - }); - } - - _onClose() { - this.closed = true; - this.debug('closed tcp socket'); - // flush any waiting connections - for (const conn of this.waitingCreateConn) { - conn(new Error('closed'), null); - } - this.waitingCreateConn = []; - this.emit('end'); - } - - // new socket connection from client for tunneling requests to client - _onConnection(socket) { - // no more socket connections allowed - if (this.connectedSockets >= this.maxTcpSockets) { - this.debug('no more sockets allowed'); - socket.destroy(); - return false; - } - - socket.once('close', (hadError) => { - this.debug('closed socket (error: %s)', hadError); - this.connectedSockets -= 1; - // remove the socket from available list - const idx = this.availableSockets.indexOf(socket); - if (idx >= 0) { - this.availableSockets.splice(idx, 1); - } - - this.debug('connected sockets: %s', this.connectedSockets); - if (this.connectedSockets <= 0) { - this.debug('all sockets disconnected'); - this.emit('offline'); - } - }); - - // close will be emitted after this - socket.once('error', (err) => { - // we do not log these errors, sessions can drop from clients for many reasons - // these are not actionable errors for our server - socket.destroy(); - }); - - if (this.connectedSockets === 0) { - this.emit('online'); - } - - this.connectedSockets += 1; - this.debug('new connection from: %s:%s', socket.address().address, socket.address().port); - - // if there are queued callbacks, give this socket now and don't queue into available - const fn = this.waitingCreateConn.shift(); - if (fn) { - this.debug('giving socket to queued conn request'); - setTimeout(() => { - fn(null, socket); - }, 0); - return; - } - - // make socket available for those waiting on sockets - this.availableSockets.push(socket); - } - - // fetch a socket from the available socket pool for the agent - // if no socket is available, queue - // cb(err, socket) - createConnection(options, cb) { - if (this.closed) { - cb(new Error('closed')); - return; - } - - this.debug('create connection'); - - // socket is a tcp connection back to the user hosting the site - const sock = this.availableSockets.shift(); - - // no available sockets - // wait until we have one - if (!sock) { - this.waitingCreateConn.push(cb); - this.debug('waiting connected: %s', this.connectedSockets); - this.debug('waiting available: %s', this.availableSockets.length); - return; - } - - this.debug('socket given'); - cb(null, sock); - } - - destroy() { - this.server.close(); - super.destroy(); - } -} - -export default TunnelAgent; diff --git a/lib/TunnelAgent.test.js b/lib/TunnelAgent.test.js deleted file mode 100644 index 9505f35..0000000 --- a/lib/TunnelAgent.test.js +++ /dev/null @@ -1,183 +0,0 @@ -import http from 'http'; -import net from 'net'; -import assert from 'assert'; - -import TunnelAgent from './TunnelAgent'; - -describe('TunnelAgent', () => { - it('should create an empty agent', async () => { - const agent = new TunnelAgent(); - assert.equal(agent.started, false); - - const info = await agent.listen(); - assert.ok(info.port > 0); - agent.destroy(); - }); - - it('should create a new server and accept connections', async () => { - const agent = new TunnelAgent(); - assert.equal(agent.started, false); - - const info = await agent.listen(); - const sock = net.createConnection({ port: info.port }); - - // in this test we wait for the socket to be connected - await new Promise(resolve => sock.once('connect', resolve)); - - const agentSock = await new Promise((resolve, reject) => { - agent.createConnection({}, (err, sock) => { - if (err) { - reject(err); - } - resolve(sock); - }); - }); - - agentSock.write('foo'); - await new Promise(resolve => sock.once('readable', resolve)); - - assert.equal('foo', sock.read().toString()); - agent.destroy(); - sock.destroy(); - }); - - it('should reject connections over the max', async () => { - const agent = new TunnelAgent({ - maxTcpSockets: 2, - }); - assert.equal(agent.started, false); - - const info = await agent.listen(); - const sock1 = net.createConnection({ port: info.port }); - const sock2 = net.createConnection({ port: info.port }); - - // two valid socket connections - const p1 = new Promise(resolve => sock1.once('connect', resolve)); - const p2 = new Promise(resolve => sock2.once('connect', resolve)); - await Promise.all([p1, p2]); - - const sock3 = net.createConnection({ port: info.port }); - const p3 = await new Promise(resolve => sock3.once('close', resolve)); - - agent.destroy(); - sock1.destroy(); - sock2.destroy(); - sock3.destroy(); - }); - - it('should queue createConnection requests', async () => { - const agent = new TunnelAgent(); - assert.equal(agent.started, false); - - const info = await agent.listen(); - - // create a promise for the next connection - let fulfilled = false; - const waitSockPromise = new Promise((resolve, reject) => { - agent.createConnection({}, (err, sock) => { - fulfilled = true; - if (err) { - reject(err); - } - resolve(sock); - }); - }); - - // check that the next socket is not yet available - await new Promise(resolve => setTimeout(resolve, 500)); - assert(!fulfilled); - - // connect, this will make a socket available - const sock = net.createConnection({ port: info.port }); - await new Promise(resolve => sock.once('connect', resolve)); - - const anotherAgentSock = await waitSockPromise; - agent.destroy(); - sock.destroy(); - }); - - it('should should emit a online event when a socket connects', async () => { - const agent = new TunnelAgent(); - const info = await agent.listen(); - - const onlinePromise = new Promise(resolve => agent.once('online', resolve)); - - const sock = net.createConnection({ port: info.port }); - await new Promise(resolve => sock.once('connect', resolve)); - - await onlinePromise; - agent.destroy(); - sock.destroy(); - }); - - it('should emit offline event when socket disconnects', async () => { - const agent = new TunnelAgent(); - const info = await agent.listen(); - - const offlinePromise = new Promise(resolve => agent.once('offline', resolve)); - - const sock = net.createConnection({ port: info.port }); - await new Promise(resolve => sock.once('connect', resolve)); - - sock.end(); - await offlinePromise; - agent.destroy(); - sock.destroy(); - }); - - it('should emit offline event only when last socket disconnects', async () => { - const agent = new TunnelAgent(); - const info = await agent.listen(); - - const offlinePromise = new Promise(resolve => agent.once('offline', resolve)); - - const sockA = net.createConnection({ port: info.port }); - await new Promise(resolve => sockA.once('connect', resolve)); - const sockB = net.createConnection({ port: info.port }); - await new Promise(resolve => sockB.once('connect', resolve)); - - sockA.end(); - - const timeout = new Promise(resolve => setTimeout(resolve, 500)); - await Promise.race([offlinePromise, timeout]); - - sockB.end(); - await offlinePromise; - - agent.destroy(); - }); - - it('should error an http request', async () => { - class ErrorAgent extends http.Agent { - constructor() { - super(); - } - - createConnection(options, cb) { - cb(new Error('foo')); - } - } - - const agent = new ErrorAgent(); - - const opt = { - host: 'localhost', - port: 1234, - path: '/', - agent: agent, - }; - - const err = await new Promise((resolve) => { - const req = http.get(opt, (res) => {}); - req.once('error', resolve); - }); - assert.equal(err.message, 'foo'); - }); - - it('should return stats', async () => { - const agent = new TunnelAgent(); - assert.deepEqual(agent.stats(), { - connectedSockets: 0, - }); - }); -}); diff --git a/package.json b/package.json index 0693b8d..ca7d5e2 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,21 @@ { - "author": "Roman Shtylman ", - "name": "localtunnel-server", + "name": "bunl", "description": "expose localhost to the world", - "version": "0.0.8", + "version": "0.0.0", "license": "MIT", "repository": { "type": "git", - "url": "git://github.com/localtunnel/server.git" + "url": "https://github.com/rubriclab/bunl" }, "dependencies": { - "book": "1.3.3", - "debug": "3.1.0", - "esm": "3.0.34", - "human-readable-ids": "1.0.3", - "koa": "2.5.1", - "koa-router": "7.4.0", - "localenv": "0.2.2", - "optimist": "0.6.1", - "pump": "3.0.0", - "tldjs": "2.3.1" + "human-id": "^4.1.1" }, "devDependencies": { - "mocha": "5.1.1", - "node-dev": "3.1.3", - "supertest": "3.1.0", - "ws": "5.1.1" + "@types/bun": "^1.0.8" }, "scripts": { - "test": "mocha --check-leaks --require esm './**/*.test.js'", - "start": "./bin/server", - "dev": "node-dev bin/server --port 3000" + "server": "bun run server.ts", + "lt": "bun run client.ts", + "demo": "bun run --hot demo.ts" } } diff --git a/server.js b/server.js deleted file mode 100644 index 21a4af6..0000000 --- a/server.js +++ /dev/null @@ -1,168 +0,0 @@ -import log from 'book'; -import Koa from 'koa'; -import tldjs from 'tldjs'; -import Debug from 'debug'; -import http from 'http'; -import { hri } from 'human-readable-ids'; -import Router from 'koa-router'; - -import ClientManager from './lib/ClientManager'; - -const debug = Debug('localtunnel:server'); - -export default function(opt) { - opt = opt || {}; - - const validHosts = (opt.domain) ? [opt.domain] : undefined; - const myTldjs = tldjs.fromUserSettings({ validHosts }); - const landingPage = opt.landing || 'https://localtunnel.github.io/www/'; - - function GetClientIdFromHostname(hostname) { - return myTldjs.getSubdomain(hostname); - } - - const manager = new ClientManager(opt); - - const schema = opt.secure ? 'https' : 'http'; - - const app = new Koa(); - const router = new Router(); - - router.get('/api/status', async (ctx, next) => { - const stats = manager.stats; - ctx.body = { - tunnels: stats.tunnels, - mem: process.memoryUsage(), - }; - }); - - router.get('/api/tunnels/:id/status', async (ctx, next) => { - const clientId = ctx.params.id; - const client = manager.getClient(clientId); - if (!client) { - ctx.throw(404); - return; - } - - const stats = client.stats(); - ctx.body = { - connected_sockets: stats.connectedSockets, - }; - }); - - app.use(router.routes()); - app.use(router.allowedMethods()); - - // root endpoint - app.use(async (ctx, next) => { - const path = ctx.request.path; - - // skip anything not on the root path - if (path !== '/') { - await next(); - return; - } - - const isNewClientRequest = ctx.query['new'] !== undefined; - if (isNewClientRequest) { - const reqId = hri.random(); - debug('making new client with id %s', reqId); - const info = await manager.newClient(reqId); - - const url = schema + '://' + info.id + '.' + ctx.request.host; - info.url = url; - ctx.body = info; - return; - } - - // no new client request, send to landing page - ctx.redirect(landingPage); - }); - - // anything after the / path is a request for a specific client name - // This is a backwards compat feature - app.use(async (ctx, next) => { - const parts = ctx.request.path.split('/'); - - // any request with several layers of paths is not allowed - // rejects /foo/bar - // allow /foo - if (parts.length !== 2) { - await next(); - return; - } - - const reqId = parts[1]; - - // limit requested hostnames to 63 characters - if (! /^(?:[a-z0-9][a-z0-9\-]{4,63}[a-z0-9]|[a-z0-9]{4,63})$/.test(reqId)) { - const msg = 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'; - ctx.status = 403; - ctx.body = { - message: msg, - }; - return; - } - - debug('making new client with id %s', reqId); - const info = await manager.newClient(reqId); - - const url = schema + '://' + info.id + '.' + ctx.request.host; - info.url = url; - ctx.body = info; - return; - }); - - const server = http.createServer(); - - const appCallback = app.callback(); - - server.on('request', (req, res) => { - // without a hostname, we won't know who the request is for - const hostname = req.headers.host; - if (!hostname) { - res.statusCode = 400; - res.end('Host header is required'); - return; - } - - const clientId = GetClientIdFromHostname(hostname); - if (!clientId) { - appCallback(req, res); - return; - } - - const client = manager.getClient(clientId); - if (!client) { - res.statusCode = 404; - res.end('404'); - return; - } - - client.handleRequest(req, res); - }); - - server.on('upgrade', (req, socket, head) => { - const hostname = req.headers.host; - if (!hostname) { - socket.destroy(); - return; - } - - const clientId = GetClientIdFromHostname(hostname); - if (!clientId) { - socket.destroy(); - return; - } - - const client = manager.getClient(clientId); - if (!client) { - socket.destroy(); - return; - } - - client.handleUpgrade(req, socket); - }); - - return server; -}; diff --git a/server.test.js b/server.test.js deleted file mode 100644 index 10bf8bc..0000000 --- a/server.test.js +++ /dev/null @@ -1,109 +0,0 @@ -import request from 'supertest'; -import assert from 'assert'; -import { Server as WebSocketServer } from 'ws'; -import WebSocket from 'ws'; -import net from 'net'; - -import createServer from './server'; - -describe('Server', () => { - it('server starts and stops', async () => { - const server = createServer(); - await new Promise(resolve => server.listen(resolve)); - await new Promise(resolve => server.close(resolve)); - }); - - it('should redirect root requests to landing page', async () => { - const server = createServer(); - const res = await request(server).get('/'); - assert.equal('https://localtunnel.github.io/www/', res.headers.location); - }); - - it('should support custom base domains', async () => { - const server = createServer({ - domain: 'domain.example.com', - }); - - const res = await request(server).get('/'); - assert.equal('https://localtunnel.github.io/www/', res.headers.location); - }); - - it('reject long domain name requests', async () => { - const server = createServer(); - const res = await request(server).get('/thisdomainisoutsidethesizeofwhatweallowwhichissixtythreecharacters'); - assert.equal(res.body.message, 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'); - }); - - it('should upgrade websocket requests', async () => { - const hostname = 'websocket-test'; - const server = createServer({ - domain: 'example.com', - }); - await new Promise(resolve => server.listen(resolve)); - - const res = await request(server).get('/websocket-test'); - const localTunnelPort = res.body.port; - - const wss = await new Promise((resolve) => { - const wsServer = new WebSocketServer({ port: 0 }, () => { - resolve(wsServer); - }); - }); - - const websocketServerPort = wss.address().port; - - const ltSocket = net.createConnection({ port: localTunnelPort }); - const wsSocket = net.createConnection({ port: websocketServerPort }); - ltSocket.pipe(wsSocket).pipe(ltSocket); - - wss.once('connection', (ws) => { - ws.once('message', (message) => { - ws.send(message); - }); - }); - - const ws = new WebSocket('http://localhost:' + server.address().port, { - headers: { - host: hostname + '.example.com', - } - }); - - ws.on('open', () => { - ws.send('something'); - }); - - await new Promise((resolve) => { - ws.once('message', (msg) => { - assert.equal(msg, 'something'); - resolve(); - }); - }); - - wss.close(); - await new Promise(resolve => server.close(resolve)); - }); - - it('should support the /api/tunnels/:id/status endpoint', async () => { - const server = createServer(); - await new Promise(resolve => server.listen(resolve)); - - // no such tunnel yet - const res = await request(server).get('/api/tunnels/foobar-test/status'); - assert.equal(res.statusCode, 404); - - // request a new client called foobar-test - { - const res = await request(server).get('/foobar-test'); - } - - { - const res = await request(server).get('/api/tunnels/foobar-test/status'); - assert.equal(res.statusCode, 200); - assert.deepEqual(res.body, { - connected_sockets: 0, - }); - } - - await new Promise(resolve => server.close(resolve)); - }); -}); \ No newline at end of file diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..95ba200 --- /dev/null +++ b/server.ts @@ -0,0 +1,81 @@ +import { ServerWebSocket, serve } from "bun"; +import { uid, wait } from "./utils"; + +type Client = { id: string }; + +const port = 1234; +const protocol = "http"; +const domain = Bun.env.DOMAIN || `localhost:${port}`; +console.log("domain:", domain); +const clients = new Map>(); +const clientData = new Map(); + +serve({ + port, + fetch: async (req, server) => { + const reqUrl = new URL(req.url); + + if (reqUrl.searchParams.has("new")) { + const id = reqUrl.searchParams.get("subdomain") || uid(); + const upgraded = server.upgrade(req, { data: { id } }); + if (upgraded) return; + else return new Response("Upgrade failed :(", { status: 500 }); + } + + console.log("user req:", req.url); + const subdomain = reqUrl.hostname.split(".")[0]; + + if (!clients.has(subdomain)) { + console.log(`\x1b[31m${subdomain} not found \x1b[0m`); + return new Response("client not found :(", { status: 404 }); + } + + // The magic: forward the req to the client + const client = clients.get(subdomain)!; + const { method, url, headers } = req; + const path = new URL(url).pathname; + client.send(JSON.stringify({ method, path, headers })); + + // Wait for the client to cache its response above + await wait(1); + + let retries = 5; + let res = clientData.get(subdomain); + + while (!res) { + await wait(1000); + retries--; + + res = clientData.get(subdomain); + + if (retries < 1) { + console.log(`\x1b[31m${subdomain} not responding \x1b[0m`); + return new Response("client not responding :(", { status: 500 }); + } + } + + return new Response(res, { status: 200 }); + }, + websocket: { + open(ws) { + console.log("connecting to", ws.data.id); + clients.set(ws.data.id, ws); + ws.send( + JSON.stringify({ + id: ws.data.id, + url: `${protocol}://${ws.data.id}.${domain}`, + }) + ); + }, + message(ws, message) { + console.log("message from", ws.data.id, message); + clientData.set(ws.data.id, message); + }, + close(ws) { + console.log(`closing ${ws.data.id}`); + clients.delete(ws.data.id); + }, + }, +}); + +console.log(`Websocket server on port ${port}`); diff --git a/utils.ts b/utils.ts new file mode 100644 index 0000000..07f468f --- /dev/null +++ b/utils.ts @@ -0,0 +1,24 @@ +import { humanId } from "human-id"; + +export const uid = () => { + return humanId({ capitalize: false, separator: "-" }); +}; + +export const wait = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export const parseResponse = (buffer: Buffer) => { + const asString = Buffer.from(buffer).toString(); + const [responseHeaders, body] = asString.split("\r\n\r\n"); + + const headers = new Headers(); + + responseHeaders + .trim() + .split("\r\n") + .forEach((header) => { + const [name, value] = header.split(": "); + if (!name.includes("HTTP")) headers.append(name, value); + }); + + return { body, headers }; +}; diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 9ad744b..0000000 --- a/yarn.lock +++ /dev/null @@ -1,1066 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -accepts@^1.2.2: - version "1.3.5" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" - dependencies: - mime-types "~2.1.18" - negotiator "0.6.1" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - -ansicolors@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef" - -any-promise@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - -async-limiter@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - -book@1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/book/-/book-1.3.3.tgz#53654254bfe083db102e461544ae9f84be199985" - dependencies: - error-stack-parser "1.3.6" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -browser-stdout@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - -builtin-modules@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - -cardinal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-1.0.0.tgz#50e21c1b0aa37729f9377def196b5a9cec932ee9" - dependencies: - ansicolors "~0.2.1" - redeyed "~1.0.0" - -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -cli-table@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" - dependencies: - colors "1.0.3" - -cli-usage@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/cli-usage/-/cli-usage-0.1.7.tgz#eaf1c9d5b91e22482333072a12127f05cd99a3ba" - dependencies: - marked "^0.3.12" - marked-terminal "^2.0.0" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - -colors@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" - -combined-stream@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" - dependencies: - delayed-stream "~1.0.0" - -commander@2.11.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" - -commander@2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.5.0.tgz#d777b6a4d847d423e5d475da864294ac1ff5aa9d" - -component-emitter@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - -content-disposition@~0.5.0: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" - -content-type@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - -cookiejar@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a" - -cookies@~0.7.0: - version "0.7.1" - resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.1.tgz#7c8a615f5481c61ab9f16c833731bcb8f663b99b" - dependencies: - depd "~1.1.1" - keygrip "~1.0.2" - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - dependencies: - array-find-index "^1.0.1" - -dateformat@~1.0.4-1.2.3: - version "1.0.12" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" - dependencies: - get-stdin "^4.0.1" - meow "^3.3.0" - -debounce@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.1.0.tgz#6a1a4ee2a9dc4b7c24bb012558dbcdb05b37f408" - -debug@*, debug@3.1.0, debug@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - dependencies: - ms "2.0.0" - -decamelize@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - -deep-equal@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - -depd@^1.1.0, depd@~1.1.1, depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - -destroy@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - -diff@3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - -dynamic-dedupe@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.2.0.tgz#50f7c28684831ecf1c170aab67a1d5311cdd76ce" - dependencies: - xtend "~2.0.6" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - -end-of-stream@^1.1.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" - dependencies: - once "^1.4.0" - -error-ex@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" - dependencies: - is-arrayish "^0.2.1" - -error-inject@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" - -error-stack-parser@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-1.3.6.tgz#e0e73b93e417138d1cd7c0b746b1a4a14854c292" - dependencies: - stackframe "^0.3.1" - -escape-html@~1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - -esm@3.0.34: - version "3.0.34" - resolved "https://registry.yarnpkg.com/esm/-/esm-3.0.34.tgz#f86afa35c83a0f535da01d15e625e45794ebddc8" - -esprima@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9" - -extend@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" - -filewatcher@~3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/filewatcher/-/filewatcher-3.0.1.tgz#f4a1957355ddaf443ccd78a895f3d55e23c8a034" - dependencies: - debounce "^1.0.0" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -foreach@~2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - -form-data@^2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" - dependencies: - asynckit "^0.4.0" - combined-stream "1.0.6" - mime-types "^2.1.12" - -formidable@^1.1.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" - -fresh@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - -glob@7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -graceful-fs@^4.1.2: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" - -growl@1.10.3: - version "1.10.3" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" - -growly@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - dependencies: - ansi-regex "^2.0.0" - -has-flag@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" - -he@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" - -hosted-git-info@^2.1.4: - version "2.6.0" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222" - -http-assert@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.3.0.tgz#a31a5cf88c873ecbb5796907d4d6f132e8c01e4a" - dependencies: - deep-equal "~1.0.1" - http-errors "~1.6.1" - -http-errors@^1.2.8, http-errors@^1.3.1, http-errors@~1.6.1: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -human-readable-ids@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/human-readable-ids/-/human-readable-ids-1.0.3.tgz#c8c6c6e95085ccb668087b7dd767834e26ca26d4" - dependencies: - knuth-shuffle "^1.0.0" - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - dependencies: - repeating "^2.0.0" - -indexof@~0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.3, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - -is-builtin-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" - dependencies: - builtin-modules "^1.0.0" - -is-finite@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" - dependencies: - number-is-nan "^1.0.0" - -is-generator-function@^1.0.3: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" - -is-object@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/is-object/-/is-object-0.1.2.tgz#00efbc08816c33cfc4ac8251d132e10dc65098d7" - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - -is@~0.2.6: - version "0.2.7" - resolved "https://registry.yarnpkg.com/is/-/is-0.2.7.tgz#3b34a2c48f359972f35042849193ae7264b63562" - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - -keygrip@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91" - -knuth-shuffle@^1.0.0: - version "1.0.8" - resolved "https://registry.yarnpkg.com/knuth-shuffle/-/knuth-shuffle-1.0.8.tgz#929a467b0efd8d297bdcf318ca988a9f1037f80d" - -koa-compose@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" - dependencies: - any-promise "^1.1.0" - -koa-compose@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.0.0.tgz#2800a513d9c361ef0d63852b038e4f6f2d5a773c" - -koa-convert@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" - dependencies: - co "^4.6.0" - koa-compose "^3.0.0" - -koa-is-json@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" - -koa-router@7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/koa-router/-/koa-router-7.4.0.tgz#aee1f7adc02d5cb31d7d67465c9eacc825e8c5e0" - dependencies: - debug "^3.1.0" - http-errors "^1.3.1" - koa-compose "^3.0.0" - methods "^1.0.1" - path-to-regexp "^1.1.1" - urijs "^1.19.0" - -koa@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/koa/-/koa-2.5.1.tgz#79f8b95f8d72d04fe9a58a8da5ebd6d341103f9c" - dependencies: - accepts "^1.2.2" - content-disposition "~0.5.0" - content-type "^1.0.0" - cookies "~0.7.0" - debug "*" - delegates "^1.0.0" - depd "^1.1.0" - destroy "^1.0.3" - error-inject "~1.0.0" - escape-html "~1.0.1" - fresh "^0.5.2" - http-assert "^1.1.0" - http-errors "^1.2.8" - is-generator-function "^1.0.3" - koa-compose "^4.0.0" - koa-convert "^1.2.0" - koa-is-json "^1.0.0" - mime-types "^2.0.7" - on-finished "^2.1.0" - only "0.0.2" - parseurl "^1.3.0" - statuses "^1.2.0" - type-is "^1.5.5" - vary "^1.0.0" - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -localenv@0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/localenv/-/localenv-0.2.2.tgz#c508f29d3485bdc9341d3ead17f61c5abd1b0bab" - dependencies: - commander "2.5.0" - -lodash._arraycopy@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" - -lodash._arrayeach@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz#bab156b2a90d3f1bbd5c653403349e5e5933ef9e" - -lodash._baseassign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" - dependencies: - lodash._basecopy "^3.0.0" - lodash.keys "^3.0.0" - -lodash._baseclone@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz#303519bf6393fe7e42f34d8b630ef7794e3542b7" - dependencies: - lodash._arraycopy "^3.0.0" - lodash._arrayeach "^3.0.0" - lodash._baseassign "^3.0.0" - lodash._basefor "^3.0.0" - lodash.isarray "^3.0.0" - lodash.keys "^3.0.0" - -lodash._basecopy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" - -lodash._basefor@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash._basefor/-/lodash._basefor-3.0.3.tgz#7550b4e9218ef09fad24343b612021c79b4c20c2" - -lodash._bindcallback@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - -lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - -lodash.assign@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" - -lodash.clonedeep@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz#a0a1e40d82a5ea89ff5b147b8444ed63d92827db" - dependencies: - lodash._baseclone "^3.0.0" - lodash._bindcallback "^3.0.0" - -lodash.isarguments@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" - -lodash.keys@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" - dependencies: - lodash._getnative "^3.0.0" - lodash.isarguments "^3.0.0" - lodash.isarray "^3.0.0" - -lodash.toarray@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" - -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - -map-obj@^1.0.0, map-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - -marked-terminal@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-2.0.0.tgz#5eaf568be66f686541afa52a558280310a31de2d" - dependencies: - cardinal "^1.0.0" - chalk "^1.1.3" - cli-table "^0.3.1" - lodash.assign "^4.2.0" - node-emoji "^1.4.1" - -marked@^0.3.12: - version "0.3.19" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - -meow@^3.3.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - -methods@^1.0.1, methods@^1.1.1, methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - -mime-db@~1.33.0: - version "1.33.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" - -mime-types@^2.0.7, mime-types@^2.1.12, mime-types@~2.1.18: - version "2.1.18" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" - dependencies: - mime-db "~1.33.0" - -mime@^1.4.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - -minimatch@3.0.4, minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - -minimist@^1.1.1, minimist@^1.1.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - -mkdirp@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - dependencies: - minimist "0.0.8" - -mocha@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.1.1.tgz#b774c75609dac05eb48f4d9ba1d827b97fde8a7b" - dependencies: - browser-stdout "1.3.1" - commander "2.11.0" - debug "3.1.0" - diff "3.5.0" - escape-string-regexp "1.0.5" - glob "7.1.2" - growl "1.10.3" - he "1.1.1" - minimatch "3.0.4" - mkdirp "0.5.1" - supports-color "4.4.0" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - -negotiator@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" - -node-dev@3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/node-dev/-/node-dev-3.1.3.tgz#582719223ebdef5d63059e6a7fbcd2399fc0f84d" - dependencies: - dateformat "~1.0.4-1.2.3" - dynamic-dedupe "^0.2.0" - filewatcher "~3.0.0" - minimist "^1.1.3" - node-notifier "^4.0.2" - resolve "^1.0.0" - -node-emoji@^1.4.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.8.1.tgz#6eec6bfb07421e2148c75c6bba72421f8530a826" - dependencies: - lodash.toarray "^4.4.0" - -node-notifier@^4.0.2: - version "4.6.1" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-4.6.1.tgz#056d14244f3dcc1ceadfe68af9cff0c5473a33f3" - dependencies: - cli-usage "^0.1.1" - growly "^1.2.0" - lodash.clonedeep "^3.0.0" - minimist "^1.1.1" - semver "^5.1.0" - shellwords "^0.1.0" - which "^1.0.5" - -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: - version "2.4.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" - dependencies: - hosted-git-info "^2.1.4" - is-builtin-module "^1.0.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - -object-assign@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - -object-keys@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.2.0.tgz#cddec02998b091be42bf1035ae32e49f1cb6ea67" - dependencies: - foreach "~2.0.1" - indexof "~0.0.1" - is "~0.2.6" - -on-finished@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - dependencies: - ee-first "1.1.1" - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - dependencies: - wrappy "1" - -only@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" - -optimist@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - dependencies: - error-ex "^1.2.0" - -parseurl@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - dependencies: - pinkie-promise "^2.0.0" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - -path-parse@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" - -path-to-regexp@^1.1.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" - dependencies: - isarray "0.0.1" - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - -process-nextick-args@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" - -pump@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - -qs@^6.5.1: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -readable-stream@^2.0.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - -redeyed@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-1.0.1.tgz#e96c193b40c0816b00aec842698e61185e55498a" - dependencies: - esprima "~3.0.0" - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - dependencies: - is-finite "^1.0.0" - -resolve@^1.0.0: - version "1.7.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" - dependencies: - path-parse "^1.0.5" - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - -"semver@2 || 3 || 4 || 5", semver@^5.1.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - -shellwords@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" - -signal-exit@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - -spdx-correct@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" - -spdx-expression-parse@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" - -stackframe@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-0.3.1.tgz#33aa84f1177a5548c8935533cbfeb3420975f5a4" - -"statuses@>= 1.4.0 < 2", statuses@^1.2.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - dependencies: - ansi-regex "^2.0.0" - -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - dependencies: - is-utf8 "^0.2.0" - -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - dependencies: - get-stdin "^4.0.1" - -superagent@3.8.2: - version "3.8.2" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403" - dependencies: - component-emitter "^1.2.0" - cookiejar "^2.1.0" - debug "^3.1.0" - extend "^3.0.0" - form-data "^2.3.1" - formidable "^1.1.1" - methods "^1.1.1" - mime "^1.4.1" - qs "^6.5.1" - readable-stream "^2.0.5" - -supertest@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.1.0.tgz#f9ebaf488e60f2176021ec580bdd23ad269e7bc6" - dependencies: - methods "~1.1.2" - superagent "3.8.2" - -supports-color@4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" - dependencies: - has-flag "^2.0.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - -tldjs@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tldjs/-/tldjs-2.3.1.tgz#cf09c3eb5d7403a9e214b7d65f3cf9651c0ab039" - dependencies: - punycode "^1.4.1" - -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - -type-is@^1.5.5: - version "1.6.16" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" - dependencies: - media-typer "0.3.0" - mime-types "~2.1.18" - -urijs@^1.19.0: - version "1.19.1" - resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a" - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - -validate-npm-package-license@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -vary@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - -which@^1.0.5: - version "1.3.0" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" - dependencies: - isexe "^2.0.0" - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - -ws@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.1.1.tgz#1d43704689711ac1942fd2f283e38f825c4b8b95" - dependencies: - async-limiter "~1.0.0" - -xtend@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.0.6.tgz#5ea657a6dba447069c2e59c58a1138cb0c5e6cee" - dependencies: - is-object "~0.1.2" - object-keys "~0.2.0"